Jelajahi Sumber

Merge branch 'refs/heads/master' into cg_live_dev

# Conflicts:
#	fs-admin/src/main/java/com/fs/his/task/Task.java
#	fs-admin/src/main/java/com/fs/live/controller/LiveAfterSalesController.java
#	fs-admin/src/main/java/com/fs/live/controller/LiveAfterSalesItemController.java
#	fs-admin/src/main/java/com/fs/live/controller/LiveAfterSalesLogsController.java
#	fs-admin/src/main/java/com/fs/live/controller/LiveAutoTaskController.java
#	fs-admin/src/main/java/com/fs/live/controller/LiveCartController.java
#	fs-admin/src/main/java/com/fs/live/controller/LiveController.java
#	fs-admin/src/main/java/com/fs/live/controller/LiveDataController.java
#	fs-admin/src/main/java/com/fs/live/controller/LiveEventConfController.java
#	fs-admin/src/main/java/com/fs/live/controller/LiveGiftController.java
#	fs-admin/src/main/java/com/fs/live/controller/LiveGoodsController.java
#	fs-admin/src/main/java/com/fs/live/controller/LiveLotteryConfController.java
#	fs-admin/src/main/java/com/fs/live/controller/LiveLotteryRecordController.java
#	fs-admin/src/main/java/com/fs/live/controller/LiveLotteryRegistrationController.java
#	fs-admin/src/main/java/com/fs/live/controller/LiveOrderController.java
#	fs-admin/src/main/java/com/fs/live/controller/LiveOrderItemController.java
#	fs-admin/src/main/java/com/fs/live/controller/LiveOrderLogsController.java
#	fs-admin/src/main/java/com/fs/live/controller/LiveRedConfController.java
#	fs-admin/src/main/java/com/fs/live/controller/LiveRewardRecordController.java
#	fs-admin/src/main/java/com/fs/live/controller/LiveUserFavoriteController.java
#	fs-admin/src/main/java/com/fs/live/controller/LiveUserFollowController.java
#	fs-admin/src/main/java/com/fs/live/controller/LiveUserLikeController.java
#	fs-admin/src/main/java/com/fs/live/controller/LiveUserLotteryRecordController.java
#	fs-admin/src/main/java/com/fs/live/controller/LiveUserRedRecordController.java
#	fs-admin/src/main/java/com/fs/live/controller/LiveVideoController.java
#	fs-admin/src/main/java/com/fs/live/controller/LiveWatchConfigController.java
#	fs-admin/src/main/java/com/fs/live/controller/LiveWatchUserController.java
#	fs-common/src/main/java/com/fs/common/constant/LiveKeysConstant.java
#	fs-common/src/main/java/com/fs/common/core/redis/RedisCache.java
#	fs-common/src/main/java/com/fs/common/utils/DateUtils.java
#	fs-company/src/main/java/com/fs/company/controller/common/CommonController.java
#	fs-company/src/main/java/com/fs/company/controller/live/LiveAutoTaskController.java
#	fs-company/src/main/java/com/fs/company/controller/live/LiveController.java
#	fs-company/src/main/java/com/fs/company/controller/live/LiveDataController.java
#	fs-company/src/main/java/com/fs/company/controller/live/LiveGiftController.java
#	fs-company/src/main/java/com/fs/company/controller/live/LiveGoodsController.java
#	fs-company/src/main/java/com/fs/company/controller/live/LiveLotteryConfController.java
#	fs-company/src/main/java/com/fs/company/controller/live/LiveLotteryRecordController.java
#	fs-company/src/main/java/com/fs/company/controller/live/LiveLotteryRegistrationController.java
#	fs-company/src/main/java/com/fs/company/controller/live/LiveMsgController.java
#	fs-company/src/main/java/com/fs/company/controller/live/LiveOrderController.java
#	fs-company/src/main/java/com/fs/company/controller/live/LiveProfitController.java
#	fs-company/src/main/java/com/fs/company/controller/live/LiveRedConfController.java
#	fs-company/src/main/java/com/fs/company/controller/live/LiveRewardRecordController.java
#	fs-company/src/main/java/com/fs/company/controller/live/LiveSensitiveWordsController.java
#	fs-company/src/main/java/com/fs/company/controller/live/LiveUserFirstEntryController.java
#	fs-company/src/main/java/com/fs/company/controller/live/LiveVideoController.java
#	fs-company/src/main/java/com/fs/company/controller/live/LiveWatchConfigController.java
#	fs-company/src/main/java/com/fs/company/controller/live/LiveWatchUserController.java
#	fs-live-app/src/main/java/com/fs/app/controller/AppBaseController.java
#	fs-live-app/src/main/java/com/fs/app/controller/CommonController.java
#	fs-live-app/src/main/java/com/fs/app/controller/LiveController.java
#	fs-live-app/src/main/java/com/fs/app/controller/UserController.java
#	fs-live-app/src/main/java/com/fs/app/task/Task.java
#	fs-live-app/src/main/java/com/fs/app/vo/LiveVo.java
#	fs-live-app/src/main/java/com/fs/app/websocket/service/WebSocketServer.java
#	fs-live-app/src/main/java/com/fs/live/utils/CityTreeUtil.java
#	fs-live-app/src/main/java/com/fs/live/utils/VerifyUtils.java
#	fs-live-app/src/main/java/com/fs/live/utils/WxUtil.java
#	fs-live-app/src/main/java/com/fs/live/websocket/bean/SendMsgVo.java
#	fs-live-app/src/main/resources/application.yml
#	fs-service/src/main/java/com/fs/company/mapper/CompanyMapper.java
#	fs-service/src/main/java/com/fs/course/domain/FsUserTalent.java
#	fs-service/src/main/java/com/fs/course/mapper/FsUserTalentMapper.java
#	fs-service/src/main/java/com/fs/course/service/IFsUserTalentService.java
#	fs-service/src/main/java/com/fs/course/service/impl/FsUserTalentServiceImpl.java
#	fs-service/src/main/java/com/fs/his/domain/FsUser.java
#	fs-service/src/main/java/com/fs/his/mapper/FsStoreProductMapper.java
#	fs-service/src/main/java/com/fs/his/mapper/FsUserMapper.java
#	fs-service/src/main/java/com/fs/his/service/IFsStoreProductService.java
#	fs-service/src/main/java/com/fs/his/service/IFsUserService.java
#	fs-service/src/main/java/com/fs/his/service/impl/FsStoreProductServiceImpl.java
#	fs-service/src/main/java/com/fs/his/service/impl/FsUserServiceImpl.java
#	fs-service/src/main/java/com/fs/live/domain/Live.java
#	fs-service/src/main/java/com/fs/live/domain/LiveAfterSales.java
#	fs-service/src/main/java/com/fs/live/domain/LiveAfterSalesItem.java
#	fs-service/src/main/java/com/fs/live/domain/LiveAfterSalesLogs.java
#	fs-service/src/main/java/com/fs/live/domain/LiveAnchor.java
#	fs-service/src/main/java/com/fs/live/domain/LiveAutoTask.java
#	fs-service/src/main/java/com/fs/live/domain/LiveCart.java
#	fs-service/src/main/java/com/fs/live/domain/LiveCompanyCode.java
#	fs-service/src/main/java/com/fs/live/domain/LiveData.java
#	fs-service/src/main/java/com/fs/live/domain/LiveEventConf.java
#	fs-service/src/main/java/com/fs/live/domain/LiveGift.java
#	fs-service/src/main/java/com/fs/live/domain/LiveGoods.java
#	fs-service/src/main/java/com/fs/live/domain/LiveLotteryConf.java
#	fs-service/src/main/java/com/fs/live/domain/LiveLotteryProductConf.java
#	fs-service/src/main/java/com/fs/live/domain/LiveLotteryRecord.java
#	fs-service/src/main/java/com/fs/live/domain/LiveLotteryRegistration.java
#	fs-service/src/main/java/com/fs/live/domain/LiveOrder.java
#	fs-service/src/main/java/com/fs/live/domain/LiveOrderItem.java
#	fs-service/src/main/java/com/fs/live/domain/LiveOrderLogs.java
#	fs-service/src/main/java/com/fs/live/domain/LiveOrderPayment.java
#	fs-service/src/main/java/com/fs/live/domain/LiveRedConf.java
#	fs-service/src/main/java/com/fs/live/domain/LiveRewardRecord.java
#	fs-service/src/main/java/com/fs/live/domain/LiveSensitiveWords.java
#	fs-service/src/main/java/com/fs/live/domain/LiveUserFavorite.java
#	fs-service/src/main/java/com/fs/live/domain/LiveUserFirstEntry.java
#	fs-service/src/main/java/com/fs/live/domain/LiveUserFollow.java
#	fs-service/src/main/java/com/fs/live/domain/LiveUserGift.java
#	fs-service/src/main/java/com/fs/live/domain/LiveUserLike.java
#	fs-service/src/main/java/com/fs/live/domain/LiveUserLotteryRecord.java
#	fs-service/src/main/java/com/fs/live/domain/LiveUserRedRecord.java
#	fs-service/src/main/java/com/fs/live/domain/LiveWatchConfig.java
#	fs-service/src/main/java/com/fs/live/domain/LiveWatchUser.java
#	fs-service/src/main/java/com/fs/live/dto/LiveOrderItemDTO.java
#	fs-service/src/main/java/com/fs/live/enums/LiveOrderCancleReason.java
#	fs-service/src/main/java/com/fs/live/mapper/LiveAfterSalesItemMapper.java
#	fs-service/src/main/java/com/fs/live/mapper/LiveAfterSalesLogsMapper.java
#	fs-service/src/main/java/com/fs/live/mapper/LiveAfterSalesMapper.java
#	fs-service/src/main/java/com/fs/live/mapper/LiveAnchorMapper.java
#	fs-service/src/main/java/com/fs/live/mapper/LiveAutoTaskMapper.java
#	fs-service/src/main/java/com/fs/live/mapper/LiveCartMapper.java
#	fs-service/src/main/java/com/fs/live/mapper/LiveCompanyCodeMapper.java
#	fs-service/src/main/java/com/fs/live/mapper/LiveDataMapper.java
#	fs-service/src/main/java/com/fs/live/mapper/LiveEventConfMapper.java
#	fs-service/src/main/java/com/fs/live/mapper/LiveGiftMapper.java
#	fs-service/src/main/java/com/fs/live/mapper/LiveGoodsMapper.java
#	fs-service/src/main/java/com/fs/live/mapper/LiveLotteryConfMapper.java
#	fs-service/src/main/java/com/fs/live/mapper/LiveLotteryProductConfMapper.java
#	fs-service/src/main/java/com/fs/live/mapper/LiveLotteryRecordMapper.java
#	fs-service/src/main/java/com/fs/live/mapper/LiveLotteryRegistrationMapper.java
#	fs-service/src/main/java/com/fs/live/mapper/LiveMapper.java
#	fs-service/src/main/java/com/fs/live/mapper/LiveMsgMapper.java
#	fs-service/src/main/java/com/fs/live/mapper/LiveOrderItemMapper.java
#	fs-service/src/main/java/com/fs/live/mapper/LiveOrderLogsMapper.java
#	fs-service/src/main/java/com/fs/live/mapper/LiveOrderMapper.java
#	fs-service/src/main/java/com/fs/live/mapper/LiveOrderPaymentMapper.java
#	fs-service/src/main/java/com/fs/live/mapper/LiveRedConfMapper.java
#	fs-service/src/main/java/com/fs/live/mapper/LiveRewardRecordMapper.java
#	fs-service/src/main/java/com/fs/live/mapper/LiveSensitiveWordsMapper.java
#	fs-service/src/main/java/com/fs/live/mapper/LiveUserFavoriteMapper.java
#	fs-service/src/main/java/com/fs/live/mapper/LiveUserFirstEntryMapper.java
#	fs-service/src/main/java/com/fs/live/mapper/LiveUserFollowMapper.java
#	fs-service/src/main/java/com/fs/live/mapper/LiveUserGiftMapper.java
#	fs-service/src/main/java/com/fs/live/mapper/LiveUserLikeMapper.java
#	fs-service/src/main/java/com/fs/live/mapper/LiveUserLotteryRecordMapper.java
#	fs-service/src/main/java/com/fs/live/mapper/LiveUserRedRecordMapper.java
#	fs-service/src/main/java/com/fs/live/mapper/LiveVideoMapper.java
#	fs-service/src/main/java/com/fs/live/mapper/LiveWatchConfigMapper.java
#	fs-service/src/main/java/com/fs/live/mapper/LiveWatchUserMapper.java
#	fs-service/src/main/java/com/fs/live/param/LiveAfterSalesListUParam.java
#	fs-service/src/main/java/com/fs/live/param/LiveLotteryProduct.java
#	fs-service/src/main/java/com/fs/live/param/LiveLotteryProductSaveParam.java
#	fs-service/src/main/java/com/fs/live/param/LiveOrderSearchParam.java
#	fs-service/src/main/java/com/fs/live/service/ILiveAfterSalesItemService.java
#	fs-service/src/main/java/com/fs/live/service/ILiveAfterSalesLogsService.java
#	fs-service/src/main/java/com/fs/live/service/ILiveAfterSalesService.java
#	fs-service/src/main/java/com/fs/live/service/ILiveAnchorService.java
#	fs-service/src/main/java/com/fs/live/service/ILiveAutoTaskService.java
#	fs-service/src/main/java/com/fs/live/service/ILiveCartService.java
#	fs-service/src/main/java/com/fs/live/service/ILiveCompanyCodeService.java
#	fs-service/src/main/java/com/fs/live/service/ILiveDataService.java
#	fs-service/src/main/java/com/fs/live/service/ILiveEventConfService.java
#	fs-service/src/main/java/com/fs/live/service/ILiveGiftService.java
#	fs-service/src/main/java/com/fs/live/service/ILiveGoodsService.java
#	fs-service/src/main/java/com/fs/live/service/ILiveLotteryConfService.java
#	fs-service/src/main/java/com/fs/live/service/ILiveLotteryRecordService.java
#	fs-service/src/main/java/com/fs/live/service/ILiveLotteryRegistrationService.java
#	fs-service/src/main/java/com/fs/live/service/ILiveMsgService.java
#	fs-service/src/main/java/com/fs/live/service/ILiveOrderItemService.java
#	fs-service/src/main/java/com/fs/live/service/ILiveOrderLogsService.java
#	fs-service/src/main/java/com/fs/live/service/ILiveOrderPaymentService.java
#	fs-service/src/main/java/com/fs/live/service/ILiveOrderService.java
#	fs-service/src/main/java/com/fs/live/service/ILiveRedConfService.java
#	fs-service/src/main/java/com/fs/live/service/ILiveRewardRecordService.java
#	fs-service/src/main/java/com/fs/live/service/ILiveSensitiveWordsService.java
#	fs-service/src/main/java/com/fs/live/service/ILiveService.java
#	fs-service/src/main/java/com/fs/live/service/ILiveUserFavoriteService.java
#	fs-service/src/main/java/com/fs/live/service/ILiveUserFirstEntryService.java
#	fs-service/src/main/java/com/fs/live/service/ILiveUserFollowService.java
#	fs-service/src/main/java/com/fs/live/service/ILiveUserGiftService.java
#	fs-service/src/main/java/com/fs/live/service/ILiveUserLikeService.java
#	fs-service/src/main/java/com/fs/live/service/ILiveUserLotteryRecordService.java
#	fs-service/src/main/java/com/fs/live/service/ILiveUserRedRecordService.java
#	fs-service/src/main/java/com/fs/live/service/ILiveVideoService.java
#	fs-service/src/main/java/com/fs/live/service/ILiveWatchConfigService.java
#	fs-service/src/main/java/com/fs/live/service/ILiveWatchUserService.java
#	fs-service/src/main/java/com/fs/live/service/impl/LiveAfterSalesItemServiceImpl.java
#	fs-service/src/main/java/com/fs/live/service/impl/LiveAfterSalesLogsServiceImpl.java
#	fs-service/src/main/java/com/fs/live/service/impl/LiveAfterSalesServiceImpl.java
#	fs-service/src/main/java/com/fs/live/service/impl/LiveAnchorServiceImpl.java
#	fs-service/src/main/java/com/fs/live/service/impl/LiveAutoTaskServiceImpl.java
#	fs-service/src/main/java/com/fs/live/service/impl/LiveCartServiceImpl.java
#	fs-service/src/main/java/com/fs/live/service/impl/LiveCompanyCodeServiceImpl.java
#	fs-service/src/main/java/com/fs/live/service/impl/LiveDataServiceImpl.java
#	fs-service/src/main/java/com/fs/live/service/impl/LiveEventConfServiceImpl.java
#	fs-service/src/main/java/com/fs/live/service/impl/LiveGiftServiceImpl.java
#	fs-service/src/main/java/com/fs/live/service/impl/LiveGoodsServiceImpl.java
#	fs-service/src/main/java/com/fs/live/service/impl/LiveLotteryConfServiceImpl.java
#	fs-service/src/main/java/com/fs/live/service/impl/LiveLotteryRecordServiceImpl.java
#	fs-service/src/main/java/com/fs/live/service/impl/LiveLotteryRegistrationServiceImpl.java
#	fs-service/src/main/java/com/fs/live/service/impl/LiveMsgServiceImpl.java
#	fs-service/src/main/java/com/fs/live/service/impl/LiveOrderItemServiceImpl.java
#	fs-service/src/main/java/com/fs/live/service/impl/LiveOrderLogsServiceImpl.java
#	fs-service/src/main/java/com/fs/live/service/impl/LiveOrderPaymentServiceImpl.java
#	fs-service/src/main/java/com/fs/live/service/impl/LiveOrderServiceImpl.java
#	fs-service/src/main/java/com/fs/live/service/impl/LiveRedConfServiceImpl.java
#	fs-service/src/main/java/com/fs/live/service/impl/LiveRewardRecordServiceImpl.java
#	fs-service/src/main/java/com/fs/live/service/impl/LiveSensitiveWordsServiceImpl.java
#	fs-service/src/main/java/com/fs/live/service/impl/LiveServiceImpl.java
#	fs-service/src/main/java/com/fs/live/service/impl/LiveUserFavoriteServiceImpl.java
#	fs-service/src/main/java/com/fs/live/service/impl/LiveUserFirstEntryServiceImpl.java
#	fs-service/src/main/java/com/fs/live/service/impl/LiveUserFollowServiceImpl.java
#	fs-service/src/main/java/com/fs/live/service/impl/LiveUserGiftServiceImpl.java
#	fs-service/src/main/java/com/fs/live/service/impl/LiveUserLikeServiceImpl.java
#	fs-service/src/main/java/com/fs/live/service/impl/LiveUserLotteryRecordServiceImpl.java
#	fs-service/src/main/java/com/fs/live/service/impl/LiveUserRedRecordServiceImpl.java
#	fs-service/src/main/java/com/fs/live/service/impl/LiveVideoServiceImpl.java
#	fs-service/src/main/java/com/fs/live/service/impl/LiveWatchConfigServiceImpl.java
#	fs-service/src/main/java/com/fs/live/service/impl/LiveWatchUserServiceImpl.java
#	fs-service/src/main/java/com/fs/live/vo/ColumnsConfigVo.java
#	fs-service/src/main/java/com/fs/live/vo/LiveAfterSalesListUVO.java
#	fs-service/src/main/java/com/fs/live/vo/LiveCartVo.java
#	fs-service/src/main/java/com/fs/live/vo/LiveGoodsListVo.java
#	fs-service/src/main/java/com/fs/live/vo/LiveGoodsVo.java
#	fs-service/src/main/java/com/fs/live/vo/LiveLotteryConfVo.java
#	fs-service/src/main/java/com/fs/live/vo/LiveLotteryProductListVo.java
#	fs-service/src/main/java/com/fs/live/vo/LiveOrderItemListUVO.java
#	fs-service/src/main/java/com/fs/live/vo/LiveOrderListVo.java
#	fs-service/src/main/java/com/fs/live/vo/LiveQuestionLiveVO.java
#	fs-service/src/main/java/com/fs/live/vo/LiveUserFirstProfit.java
#	fs-service/src/main/java/com/fs/live/vo/LiveWatchUserVO.java
#	fs-service/src/main/resources/mapper/course/FsUserTalentMapper.xml
#	fs-service/src/main/resources/mapper/live/LiveAfterSalesMapper.xml
#	fs-service/src/main/resources/mapper/live/LiveAutoTaskMapper.xml
#	fs-service/src/main/resources/mapper/live/LiveCartMapper.xml
#	fs-service/src/main/resources/mapper/live/LiveDataMapper.xml
#	fs-service/src/main/resources/mapper/live/LiveGiftMapper.xml
#	fs-service/src/main/resources/mapper/live/LiveGoodsMapper.xml
#	fs-service/src/main/resources/mapper/live/LiveLotteryProductConfMapper.xml
#	fs-service/src/main/resources/mapper/live/LiveLotteryRegistrationMapper.xml
#	fs-service/src/main/resources/mapper/live/LiveMapper.xml
#	fs-service/src/main/resources/mapper/live/LiveMsgMapper.xml
#	fs-service/src/main/resources/mapper/live/LiveOrderItemMapper.xml
#	fs-service/src/main/resources/mapper/live/LiveOrderMapper.xml
#	fs-service/src/main/resources/mapper/live/LiveOrderPaymentErrorMapper.xml
#	fs-service/src/main/resources/mapper/live/LiveOrderPaymentMapper.xml
#	fs-service/src/main/resources/mapper/live/LiveRewardRecordMapper.xml
#	fs-service/src/main/resources/mapper/live/LiveUserFirstEntryMapper.xml
#	fs-service/src/main/resources/mapper/live/LiveUserLotteryRecordMapper.xml
#	fs-service/src/main/resources/mapper/live/LiveUserRedRecordMapper.xml
#	fs-service/src/main/resources/mapper/live/LiveWatchUserMapper.xml
#	fs-user-app/src/main/java/com/fs/app/controller/HuifuPayController.java
#	fs-user-app/src/main/java/com/fs/app/vo/LotteryVo.java
yuhongqi 1 bulan lalu
induk
melakukan
3a1bac0123
100 mengubah file dengan 7336 tambahan dan 276 penghapusan
  1. 1 0
      DirectoryV3.xml
  2. 59 13
      README.md
  3. 139 0
      deploy - test.sh
  4. 139 0
      deploy.sh
  5. 37 0
      fs-ad-api/src/main/java/com/fs/framework/config/RedisConfig.java
  6. 6 0
      fs-ad-new-api/Dockerfile
  7. 98 0
      fs-ad-new-api/pom.xml
  8. 14 0
      fs-ad-new-api/src/main/java/com/fs/FSServletInitializer.java
  9. 22 0
      fs-ad-new-api/src/main/java/com/fs/FsAdNewApiApplication.java
  10. 130 0
      fs-ad-new-api/src/main/java/com/fs/app/controller/CallbackController.java
  11. 51 0
      fs-ad-new-api/src/main/java/com/fs/app/controller/LandingPageController.java
  12. 42 0
      fs-ad-new-api/src/main/java/com/fs/app/controller/TestController.java
  13. 48 0
      fs-ad-new-api/src/main/java/com/fs/app/controller/TrackingController.java
  14. 88 0
      fs-ad-new-api/src/main/java/com/fs/app/controller/WeChatController.java
  15. 29 0
      fs-ad-new-api/src/main/java/com/fs/app/facade/CallbackProcessingFacadeService.java
  16. 294 0
      fs-ad-new-api/src/main/java/com/fs/app/facade/CallbackProcessingFacadeServiceImpl.java
  17. 172 0
      fs-ad-new-api/src/main/java/com/fs/app/facade/ConversionServiceImpl.java
  18. 23 0
      fs-ad-new-api/src/main/java/com/fs/app/facade/IConversionService.java
  19. 71 0
      fs-ad-new-api/src/main/java/com/fs/app/mq/consumer/ConversionTrackingMessageConsumer.java
  20. 91 0
      fs-ad-new-api/src/main/java/com/fs/app/task/ConversionRetryTask.java
  21. 225 0
      fs-ad-new-api/src/main/java/com/fs/app/task/DataSyncTask.java
  22. 110 0
      fs-ad-new-api/src/main/java/com/fs/framework/aspectj/ControllerLogAspect.java
  23. 182 0
      fs-ad-new-api/src/main/java/com/fs/framework/aspectj/DataScopeAspect.java
  24. 73 0
      fs-ad-new-api/src/main/java/com/fs/framework/aspectj/DataSourceAspect.java
  25. 117 0
      fs-ad-new-api/src/main/java/com/fs/framework/aspectj/DistributeLockAspect.java
  26. 244 0
      fs-ad-new-api/src/main/java/com/fs/framework/aspectj/LogAspect.java
  27. 58 0
      fs-ad-new-api/src/main/java/com/fs/framework/aspectj/ScheduledTaskAspect.java
  28. 29 0
      fs-ad-new-api/src/main/java/com/fs/framework/aspectj/ScheduledTraceIdAspect.java
  29. 0 0
      fs-ad-new-api/src/main/java/com/fs/framework/config/ApplicationConfig.java
  30. 61 0
      fs-ad-new-api/src/main/java/com/fs/framework/config/AsyncConfig.java
  31. 19 0
      fs-ad-new-api/src/main/java/com/fs/framework/config/CorsConfig.java
  32. 92 0
      fs-ad-new-api/src/main/java/com/fs/framework/config/DataSourceConfig.java
  33. 72 0
      fs-ad-new-api/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java
  34. 0 0
      fs-ad-new-api/src/main/java/com/fs/framework/config/FilterConfig.java
  35. 30 0
      fs-ad-new-api/src/main/java/com/fs/framework/config/MdcTaskDecorator.java
  36. 148 0
      fs-ad-new-api/src/main/java/com/fs/framework/config/MyBatisConfig.java
  37. 158 0
      fs-ad-new-api/src/main/java/com/fs/framework/config/RedisConfig.java
  38. 50 0
      fs-ad-new-api/src/main/java/com/fs/framework/config/SecurityConfig.java
  39. 33 0
      fs-ad-new-api/src/main/java/com/fs/framework/config/ServerConfig.java
  40. 0 0
      fs-ad-new-api/src/main/java/com/fs/framework/config/ThreadPoolConfig.java
  41. 36 0
      fs-ad-new-api/src/main/java/com/fs/framework/config/TrackIdFilter.java
  42. 0 0
      fs-ad-new-api/src/main/java/com/fs/framework/config/properties/DruidProperties.java
  43. 27 0
      fs-ad-new-api/src/main/java/com/fs/framework/datasource/DynamicDataSource.java
  44. 45 0
      fs-ad-new-api/src/main/java/com/fs/framework/datasource/DynamicDataSourceContextHolder.java
  45. 56 0
      fs-ad-new-api/src/main/java/com/fs/framework/manager/AsyncManager.java
  46. 0 0
      fs-ad-new-api/src/main/java/com/fs/framework/manager/ShutdownManager.java
  47. 103 0
      fs-ad-new-api/src/main/java/com/fs/framework/manager/factory/AsyncFactory.java
  48. 1 0
      fs-ad-new-api/src/main/resources/META-INF/spring-devtools.properties
  49. 10 0
      fs-ad-new-api/src/main/resources/application.yml
  50. 0 0
      fs-ad-new-api/src/main/resources/banner.txt
  51. 0 0
      fs-ad-new-api/src/main/resources/i18n/messages.properties
  52. 93 0
      fs-ad-new-api/src/main/resources/logback.xml
  53. 15 0
      fs-ad-new-api/src/main/resources/mybatis/mybatis-config.xml
  54. 6 0
      fs-admin/Dockerfile
  55. 4 0
      fs-admin/pom.xml
  56. 3 8
      fs-admin/src/main/java/com/fs/FSApplication.java
  57. 706 36
      fs-admin/src/main/java/com/fs/api/controller/IndexStatisticsController.java
  58. 78 0
      fs-admin/src/main/java/com/fs/api/controller/StatisticManageController.java
  59. 48 0
      fs-admin/src/main/java/com/fs/company/controller/CompanyConfigController.java
  60. 68 5
      fs-admin/src/main/java/com/fs/company/controller/CompanyController.java
  61. 5 0
      fs-admin/src/main/java/com/fs/company/controller/CompanyDeductController.java
  62. 1 0
      fs-admin/src/main/java/com/fs/company/controller/CompanyRechargeController.java
  63. 103 0
      fs-admin/src/main/java/com/fs/company/controller/CompanyRedPacketBalanceLogsController.java
  64. 108 0
      fs-admin/src/main/java/com/fs/company/controller/CompanyStatisticsController.java
  65. 103 0
      fs-admin/src/main/java/com/fs/company/controller/CompanyTrafficController.java
  66. 49 0
      fs-admin/src/main/java/com/fs/company/controller/CompanyTrafficLogController.java
  67. 572 0
      fs-admin/src/main/java/com/fs/company/controller/CompanyUserAllController.java
  68. 41 0
      fs-admin/src/main/java/com/fs/course/business/FsVideoResourceBusinessService.java
  69. 41 0
      fs-admin/src/main/java/com/fs/course/controller/CourseRedPacketStatisticsController.java
  70. 18 6
      fs-admin/src/main/java/com/fs/course/controller/FsCourseAnswerLogsController.java
  71. 38 0
      fs-admin/src/main/java/com/fs/course/controller/FsCourseFinishTempController.java
  72. 70 5
      fs-admin/src/main/java/com/fs/course/controller/FsCoursePlaySourceConfigController.java
  73. 35 8
      fs-admin/src/main/java/com/fs/course/controller/FsCourseQuestionBankController.java
  74. 77 20
      fs-admin/src/main/java/com/fs/course/controller/FsCourseRedPacketLogController.java
  75. 35 1
      fs-admin/src/main/java/com/fs/course/controller/FsCourseTrafficLogController.java
  76. 70 8
      fs-admin/src/main/java/com/fs/course/controller/FsCourseWatchLogController.java
  77. 44 0
      fs-admin/src/main/java/com/fs/course/controller/FsUserCourseCategoryController.java
  78. 184 0
      fs-admin/src/main/java/com/fs/course/controller/FsUserCourseCompanyStatisticsController.java
  79. 94 72
      fs-admin/src/main/java/com/fs/course/controller/FsUserCourseController.java
  80. 45 0
      fs-admin/src/main/java/com/fs/course/controller/FsUserCoursePeriodController.java
  81. 41 0
      fs-admin/src/main/java/com/fs/course/controller/FsUserCourseVideoController.java
  82. 65 5
      fs-admin/src/main/java/com/fs/course/controller/FsUserVideoController.java
  83. 59 23
      fs-admin/src/main/java/com/fs/course/controller/FsVideoResourceController.java
  84. 67 0
      fs-admin/src/main/java/com/fs/course/controller/qw/QwFsCourseWatchLogController.java
  85. 21 0
      fs-admin/src/main/java/com/fs/course/params/FsUserCourseConfigParam.java
  86. 17 0
      fs-admin/src/main/java/com/fs/course/task/CourseStatisticsTask.java
  87. 37 0
      fs-admin/src/main/java/com/fs/course/task/RedPacketLogsTask.java
  88. 31 0
      fs-admin/src/main/java/com/fs/course/task/WatchCourseStatistics.java
  89. 15 13
      fs-admin/src/main/java/com/fs/crm/controller/CrmCustomerController.java
  90. 1 2
      fs-admin/src/main/java/com/fs/fastGpt/FastgptExtUserTagController.java
  91. 6 6
      fs-admin/src/main/java/com/fs/his/controller/FsAdvController.java
  92. 6 6
      fs-admin/src/main/java/com/fs/his/controller/FsArticleController.java
  93. 144 9
      fs-admin/src/main/java/com/fs/his/controller/FsCompanyController.java
  94. 18 13
      fs-admin/src/main/java/com/fs/his/controller/FsCompanyDeductController.java
  95. 127 0
      fs-admin/src/main/java/com/fs/his/controller/FsCompanyDivItemController.java
  96. 20 13
      fs-admin/src/main/java/com/fs/his/controller/FsCompanyRechargeController.java
  97. 124 0
      fs-admin/src/main/java/com/fs/his/controller/FsComplaintTemplateController.java
  98. 103 0
      fs-admin/src/main/java/com/fs/his/controller/FsDfAccountController.java
  99. 49 4
      fs-admin/src/main/java/com/fs/his/controller/FsDoctorController.java
  100. 98 0
      fs-admin/src/main/java/com/fs/his/controller/FsHisComplaintController.java

+ 1 - 0
DirectoryV3.xml

@@ -5,4 +5,5 @@
      <tree path="/fs-admin/src/main/java/com/fs/company/controller" title="公司"/>
      <tree path="/fs-admin/src/main/java/com/fs/live/controller" title="直播"/>
      <tree path="/fs-admin/src/main/java/com/fs/qw" title="企微"/>
+     <tree path="/fs-ad-api" title="广告回传"/>
  </trees>

+ 59 - 13
README.md

@@ -3,21 +3,33 @@
 #### 介绍
 问诊平台
 
-#### 软件架构
-软件架构说明
+#### 软件模块说明
+
+| 模块名称         | 模块描述          | 对应前端项目                  |
+|--------------|---------------|-------------------------|
+| fs-admin     | 总后台服务         | ylrz_his_scrm_adminUI   |
+| fs-company   | 销售端           | ylrz_his_scrm_companyUI |
+| fs-store     | 店铺端           | ylrz_his_scrm_storeUI |
+| fs-doctor    | 医生端           | ylrz_his_scrm_doctorUI |
+| fs-user-app  | 微信小程序端        | 对应某个微信小程序(前端蒲瑶清楚)       |
+| fs-framework | 主要依赖包,核心包     | /                       |
+| fs-service   | 所有的链接配置文件都在里面 | /                       |
+
+#### 分支说明
+| 分支名称       | 描述    | 对应前端项目与分支                                                                |
+|------------|-------|--------------------------------------------------------------------------|
+| ScrmStores | 医建宝项目 | ylrz_his_scrm_adminUI(yjb_ScrmStores)、ylrz_his_scrm_companyUI(ScrmStore) |
 
 
 #### 安装教程
 
 1.  xxxx
 2.  xxxx
-3.  xxxx
 
 #### 使用说明
 
-1.  xxxx
-2.  xxxx
-3.  xxxx
+1.  注意调整Memory的大小,以及堆内存大小
+2.  对于maven仓库缺少的jar包引用,需要拷贝现有的文件(拷贝后依然出现错误,直接删除错误包下的_remote.repositories文件)。
 
 #### 参与贡献
 
@@ -27,11 +39,45 @@
 4.  新建 Pull Request
 
 
-#### 特技
+#### sql更新
+
+-- 添加字段(修改企微的备注手机号码搜索时用到)
+
+ALTER TABLE qw_external_contact
+ADD COLUMN search_mobile char(44)
+GENERATED ALWAYS AS (
+CAST(
+REGEXP_REPLACE(
+COALESCE(JSON_UNQUOTE(JSON_EXTRACT(remark_mobiles, '$[0]')), ''),
+'[^0-9]', ''
+) AS CHAR(44)
+)
+) VIRTUAL;
+
+-- 创建索引
+
+CREATE INDEX idx_search_mobile ON qw_external_contact(search_mobile);
+
 
-1.  使用 Readme\_XXX.md 来支持不同的语言,例如 Readme\_en.md, Readme\_zh.md
-2.  Gitee 官方博客 [blog.gitee.com](https://blog.gitee.com)
-3.  你可以 [https://gitee.com/explore](https://gitee.com/explore) 这个地址来了解 Gitee 上的优秀开源项目
-4.  [GVP](https://gitee.com/gvp) 全称是 Gitee 最有价值开源项目,是综合评定出的优秀开源项目
-5.  Gitee 官方提供的使用手册 [https://gitee.com/help](https://gitee.com/help)
-6.  Gitee 封面人物是一档用来展示 Gitee 会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)
+-- 新加统计表
+CREATE TABLE IF NOT EXISTS user_daily_stats (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '记录ID',
+    company_id BIGINT NOT NULL COMMENT '公司ID',
+    company_name VARCHAR(255) NOT NULL COMMENT '公司名称',
+    dept_id BIGINT NOT NULL COMMENT '部门ID',
+    dept_name VARCHAR(255) NOT NULL COMMENT '部门名称',
+    user_id BIGINT COMMENT '用户ID',
+    user_name VARCHAR(100) COMMENT '用户名',
+    nick_name VARCHAR(100) COMMENT '用户昵称',
+    statistics_time DATE NOT NULL COMMENT '统计日期',
+    line_num INT NOT NULL DEFAULT 0 COMMENT 't1统计数(线路数)',
+    active_num INT NOT NULL DEFAULT 0 COMMENT 't2统计数(活跃数)',
+    complete_num INT NOT NULL DEFAULT 0 COMMENT '完成数(t4)',
+    answer_num INT NOT NULL DEFAULT 0 COMMENT '答题数(t5)',
+    red_packet_num INT NOT NULL DEFAULT 0 COMMENT '红包数量(t6)',
+    red_packet_amount DECIMAL(10, 2) NOT NULL DEFAULT 0.00 COMMENT '红包金额',
+    create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间',
+    update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '记录更新时间',
+    KEY idx_company_date (company_id, statistics_time) COMMENT '公司+日期索引',
+    KEY idx_dept_date (dept_id, create_time) COMMENT '部门+日期索引'
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT '用户每日统计数据表';

+ 139 - 0
deploy - test.sh

@@ -0,0 +1,139 @@
+#!/bin/bash
+
+# 各服务对应的远程服务器配置 公司本地
+declare -A SERVER_CONFIG=(
+    # 服务名:IP地址
+    ["fs-live-app"]="129.28.193.135"
+)
+
+# 通用配置(所有服务器相同)
+REMOTE_USER="root"
+REMOTE_BASE_DIR="/home/software"
+
+# 本地 JAR 包路径
+LOCAL_FS_LIVE_SOCKET_JAR="./fs-live-app/target/fs-live-app.jar"
+
+# 函数:检查本地文件是否存在
+check_local_file() {
+    if [ ! -f "$1" ]; then
+        echo "错误: 本地文件 $1 不存在。"
+        exit 1
+    fi
+}
+
+# 停止远程服务器上可能正在运行的旧版本
+stop_remote_app() {
+    local remote_user=$1
+    local remote_host=$2
+    local app_name=$3
+
+    echo "正在停止 $remote_host 上的 $app_name 服务..."
+    ssh "$remote_user@$remote_host" "pkill -f $app_name || echo '没有找到运行中的进程'"
+}
+
+# 检查服务器连通性
+check_server_connectivity() {
+    local remote_user=$1
+    local remote_host=$2
+
+    if ! ssh -o ConnectTimeout=5 "$remote_user@$remote_host" "echo '连接成功'" &>/dev/null; then
+        echo "错误: 无法连接到服务器 $remote_host"
+        return 1
+    fi
+    return 0
+}
+
+# 部署单个 JAR 包到指定服务器
+deploy_jar() {
+    local local_jar=$1
+    local service_name=$2  # 服务名,用于确定目标服务器
+    local remote_dir=$3     # 远程目录名
+
+    # 获取服务对应的服务器IP
+    local remote_host="${SERVER_CONFIG[$service_name]}"
+    if [ -z "$remote_host" ]; then
+        echo "错误: 未找到服务 $service_name 的服务器配置"
+        return 1
+    fi
+
+    local app_name=$(basename "$local_jar" .jar)
+
+    echo "========================================"
+    echo "开始部署 $service_name 到服务器 $remote_host"
+    echo "本地文件: $local_jar"
+    echo "远程目录: $REMOTE_BASE_DIR/$remote_dir"
+    echo "========================================"
+
+    # 检查本地文件
+    check_local_file "$local_jar"
+
+    # 检查服务器连通性
+    if ! check_server_connectivity "$REMOTE_USER" "$remote_host"; then
+        return 1
+    fi
+
+    # 创建远程目录(如果不存在)
+    echo "创建远程目录..."
+    ssh "$REMOTE_USER@$remote_host" "mkdir -p $REMOTE_BASE_DIR/$remote_dir"
+
+    # 停止旧版本
+    stop_remote_app "$REMOTE_USER" "$remote_host" "$app_name"
+
+    # 上传 JAR 包
+    echo "上传 JAR 包..."
+    scp "$local_jar" "$REMOTE_USER@$remote_host:$REMOTE_BASE_DIR/$remote_dir/"
+
+    # 在后台启动 JAR 包
+    echo "启动服务..."
+    ssh -f "$REMOTE_USER@$remote_host" \
+    "cd $REMOTE_BASE_DIR/$remote_dir && \
+     nohup java -jar -Xms1g -Xmx2g -XX:+UseG1GC -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 $app_name.jar --spring.profiles.active=druid-ylrz --server.port=7114  \
+     >> $app_name.log 2>&1 &"
+
+    # 检查进程是否启动成功
+    if ssh "$REMOTE_USER@$remote_host" "pgrep -f $app_name" &>/dev/null; then
+        echo "✓ $service_name 部署成功到 $remote_host"
+    else
+        echo "✗ $service_name 启动失败,请检查日志: $REMOTE_BASE_DIR/$remote_dir/$app_name.log"
+        return 1
+    fi
+
+    echo ""
+}
+
+# 主要部署流程
+echo "开始多服务器分布式部署..."
+echo "部署配置:"
+for service in "${!SERVER_CONFIG[@]}"; do
+    echo "  $service -> ${SERVER_CONFIG[$service]}"
+done
+echo ""
+
+# 部署 fs-live-app
+deploy_jar "$LOCAL_FS_LIVE_SOCKET_JAR" "fs-live-app" "fs-live-app"
+
+# 部署 fs-sync (注意:这里使用了不同的JAR命名方式)
+#deploy_jar "$LOCAL_FS_SYNC_APP_JAR" "fs-sync" "fs-sync"
+
+echo "========================================"
+echo "分布式部署完成!"
+echo "各服务部署状态:"
+for service in "${!SERVER_CONFIG[@]}"; do
+    remote_host="${SERVER_CONFIG[$service]}"
+    echo "  $service -> $remote_host"
+done
+echo "========================================"
+
+# 可选:显示各服务进程状态
+#echo "服务进程状态检查:"
+#for service in "${!SERVER_CONFIG[@]}"; do
+#    remote_host="${SERVER_CONFIG[$service]}"
+#    app_name="$service"  # 简化处理,实际可能需要根据JAR文件名调整
+#    if ssh "$REMOTE_USER@$remote_host" "pgrep -f $app_name" &>/dev/null; then
+#        echo "  ✓ $service 在 $remote_host 上运行正常"
+#    else
+#        echo "  ✗ $service 在 $remote_host 上未运行"
+#    fi
+#done
+
+# 251105 0953

+ 139 - 0
deploy.sh

@@ -0,0 +1,139 @@
+#!/bin/bash
+
+# 各服务对应的远程服务器配置 北京卓美
+declare -A SERVER_CONFIG=(
+    # 服务名:IP地址
+    ["fs-live-app"]="114.132.218.150"
+)
+
+# 通用配置(所有服务器相同)
+REMOTE_USER="root"
+REMOTE_BASE_DIR="/home/software"
+
+# 本地 JAR 包路径
+LOCAL_FS_LIVE_SOCKET_JAR="./fs-live-app/target/fs-live-app.jar"
+
+# 函数:检查本地文件是否存在
+check_local_file() {
+    if [ ! -f "$1" ]; then
+        echo "错误: 本地文件 $1 不存在。"
+        exit 1
+    fi
+}
+
+# 停止远程服务器上可能正在运行的旧版本
+stop_remote_app() {
+    local remote_user=$1
+    local remote_host=$2
+    local app_name=$3
+
+    echo "正在停止 $remote_host 上的 $app_name 服务..."
+    ssh "$remote_user@$remote_host" "pkill -f $app_name || echo '没有找到运行中的进程'"
+}
+
+# 检查服务器连通性
+check_server_connectivity() {
+    local remote_user=$1
+    local remote_host=$2
+
+    if ! ssh -o ConnectTimeout=5 "$remote_user@$remote_host" "echo '连接成功'" &>/dev/null; then
+        echo "错误: 无法连接到服务器 $remote_host"
+        return 1
+    fi
+    return 0
+}
+
+# 部署单个 JAR 包到指定服务器
+deploy_jar() {
+    local local_jar=$1
+    local service_name=$2  # 服务名,用于确定目标服务器
+    local remote_dir=$3     # 远程目录名
+
+    # 获取服务对应的服务器IP
+    local remote_host="${SERVER_CONFIG[$service_name]}"
+    if [ -z "$remote_host" ]; then
+        echo "错误: 未找到服务 $service_name 的服务器配置"
+        return 1
+    fi
+
+    local app_name=$(basename "$local_jar" .jar)
+
+    echo "========================================"
+    echo "开始部署 $service_name 到服务器 $remote_host"
+    echo "本地文件: $local_jar"
+    echo "远程目录: $REMOTE_BASE_DIR/$remote_dir"
+    echo "========================================"
+
+    # 检查本地文件
+    check_local_file "$local_jar"
+
+    # 检查服务器连通性
+    if ! check_server_connectivity "$REMOTE_USER" "$remote_host"; then
+        return 1
+    fi
+
+    # 创建远程目录(如果不存在)
+    echo "创建远程目录..."
+    ssh "$REMOTE_USER@$remote_host" "mkdir -p $REMOTE_BASE_DIR/$remote_dir"
+
+    # 停止旧版本
+    stop_remote_app "$REMOTE_USER" "$remote_host" "$app_name"
+
+    # 上传 JAR 包
+    echo "上传 JAR 包..."
+    scp "$local_jar" "$REMOTE_USER@$remote_host:$REMOTE_BASE_DIR/$remote_dir/"
+
+    # 在后台启动 JAR 包
+    echo "启动服务..."
+    ssh -f "$REMOTE_USER@$remote_host" \
+    "cd $REMOTE_BASE_DIR/$remote_dir && \
+     nohup java -jar -Xms8g -Xmx8g -XX:+UseG1GC -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 $app_name.jar --spring.profiles.active=druid-bjzm --server.port=7114  \
+     >> $app_name.log 2>&1 &"
+
+    # 检查进程是否启动成功
+    if ssh "$REMOTE_USER@$remote_host" "pgrep -f $app_name" &>/dev/null; then
+        echo "✓ $service_name 部署成功到 $remote_host"
+    else
+        echo "✗ $service_name 启动失败,请检查日志: $REMOTE_BASE_DIR/$remote_dir/$app_name.log"
+        return 1
+    fi
+
+    echo ""
+}
+
+# 主要部署流程
+echo "开始多服务器分布式部署..."
+echo "部署配置:"
+for service in "${!SERVER_CONFIG[@]}"; do
+    echo "  $service -> ${SERVER_CONFIG[$service]}"
+done
+echo ""
+
+# 部署 fs-live-app
+deploy_jar "$LOCAL_FS_LIVE_SOCKET_JAR" "fs-live-app" "fs-live-app"
+
+# 部署 fs-sync (注意:这里使用了不同的JAR命名方式)
+#deploy_jar "$LOCAL_FS_SYNC_APP_JAR" "fs-sync" "fs-sync"
+
+echo "========================================"
+echo "分布式部署完成!"
+echo "各服务部署状态:"
+for service in "${!SERVER_CONFIG[@]}"; do
+    remote_host="${SERVER_CONFIG[$service]}"
+    echo "  $service -> $remote_host"
+done
+echo "========================================"
+
+# 可选:显示各服务进程状态
+#echo "服务进程状态检查:"
+#for service in "${!SERVER_CONFIG[@]}"; do
+#    remote_host="${SERVER_CONFIG[$service]}"
+#    app_name="$service"  # 简化处理,实际可能需要根据JAR文件名调整
+#    if ssh "$REMOTE_USER@$remote_host" "pgrep -f $app_name" &>/dev/null; then
+#        echo "  ✓ $service 在 $remote_host 上运行正常"
+#    else
+#        echo "  ✗ $service 在 $remote_host 上未运行"
+#    fi
+#done
+
+# 251105 0953

+ 37 - 0
fs-ad-api/src/main/java/com/fs/framework/config/RedisConfig.java

@@ -15,6 +15,8 @@ import org.springframework.data.redis.core.script.DefaultRedisScript;
 import org.springframework.data.redis.serializer.GenericToStringSerializer;
 import org.springframework.data.redis.serializer.StringRedisSerializer;
 
+import java.math.BigDecimal;
+
 /**
  * redis配置
  *
@@ -65,6 +67,24 @@ public class RedisConfig extends CachingConfigurerSupport
         template.afterPropertiesSet();
         return template;
     }
+    @Bean
+    public RedisTemplate<String, Integer> redisTemplateForInteger(RedisConnectionFactory connectionFactory) {
+        RedisTemplate<String, Integer> template = new RedisTemplate<>();
+        template.setConnectionFactory(connectionFactory);
+
+        // 使用StringRedisSerializer来序列化和反序列化redis的key值
+        template.setKeySerializer(new StringRedisSerializer());
+
+        // 使用GenericToStringSerializer保证BigDecimal精度不丢失
+        template.setValueSerializer(new GenericToStringSerializer<>(Integer.class));
+
+        // Hash的key也采用StringRedisSerializer的序列化方式
+        template.setHashKeySerializer(new StringRedisSerializer());
+        template.setHashValueSerializer(new GenericToStringSerializer<>(Integer.class));
+
+        template.afterPropertiesSet();
+        return template;
+    }
 
     @Bean
     @SuppressWarnings(value = { "unchecked", "rawtypes" })
@@ -90,7 +110,24 @@ public class RedisConfig extends CachingConfigurerSupport
         template.afterPropertiesSet();
         return template;
     }
+    @Bean
+    public RedisTemplate<String, BigDecimal> redisTemplateForBigDecimal(RedisConnectionFactory connectionFactory) {
+        RedisTemplate<String, BigDecimal> template = new RedisTemplate<>();
+        template.setConnectionFactory(connectionFactory);
+
+        // 使用StringRedisSerializer来序列化和反序列化redis的key值
+        template.setKeySerializer(new StringRedisSerializer());
+
+        // 使用GenericToStringSerializer保证BigDecimal精度不丢失
+        template.setValueSerializer(new GenericToStringSerializer<>(BigDecimal.class));
 
+        // Hash的key也采用StringRedisSerializer的序列化方式
+        template.setHashKeySerializer(new StringRedisSerializer());
+        template.setHashValueSerializer(new GenericToStringSerializer<>(BigDecimal.class));
+
+        template.afterPropertiesSet();
+        return template;
+    }
     @Bean
     public DefaultRedisScript<Long> limitScript()
     {

+ 6 - 0
fs-ad-new-api/Dockerfile

@@ -0,0 +1,6 @@
+FROM openjdk:8-jre
+# java版本,最好使用openjdk,而不是类似于Java:1.8
+COPY ./target/fs-ad-new-api.jar fs-ad-new-api.jar
+# 向外暴露的接口,最好与项目yml文件中的端口一致
+ENTRYPOINT ["java","-jar","fs-ad-new-api.jar"]
+# 执行启动命令java -jar

+ 98 - 0
fs-ad-new-api/pom.xml

@@ -0,0 +1,98 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>fs</artifactId>
+        <groupId>com.fs</groupId>
+        <version>1.1.0</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+    <artifactId>fs-ad-new-api</artifactId>
+    <description>
+       新投流接口
+    </description>
+    <dependencies>
+
+        <!-- Mysql驱动包 -->
+        <dependency>
+            <groupId>mysql</groupId>
+            <artifactId>mysql-connector-java</artifactId>
+        </dependency>
+
+        <!-- SpringBoot Web容器 -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+
+        <!-- SpringBoot 拦截器 -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-aop</artifactId>
+        </dependency>
+
+        <!-- 阿里数据库连接池 -->
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>druid-spring-boot-starter</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.fs</groupId>
+            <artifactId>fs-service</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.rocketmq</groupId>
+            <artifactId>rocketmq-spring-boot-starter</artifactId>
+            <version>2.2.3</version>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <version>2.1.1.RELEASE</version>
+                <configuration>
+                    <fork>true</fork> <!-- 如果没有该配置,devtools不会生效 -->
+                </configuration>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>repackage</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-war-plugin</artifactId>
+                <version>3.1.0</version>
+                <configuration>
+                    <failOnMissingWebXml>false</failOnMissingWebXml>
+                    <warName>${project.artifactId}</warName>
+                </configuration>
+            </plugin>
+
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <configuration>
+                    <compilerArgs>
+                        <arg>-parameters</arg>
+                    </compilerArgs>
+                </configuration>
+            </plugin>
+
+        </plugins>
+        <finalName>${project.artifactId}</finalName>
+    </build>
+
+</project>

+ 14 - 0
fs-ad-new-api/src/main/java/com/fs/FSServletInitializer.java

@@ -0,0 +1,14 @@
+package com.fs;
+
+import org.springframework.boot.builder.SpringApplicationBuilder;
+import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
+
+
+public class FSServletInitializer extends SpringBootServletInitializer
+{
+    @Override
+    protected SpringApplicationBuilder configure(SpringApplicationBuilder application)
+    {
+        return application.sources(FsAdNewApiApplication.class);
+    }
+}

+ 22 - 0
fs-ad-new-api/src/main/java/com/fs/FsAdNewApiApplication.java

@@ -0,0 +1,22 @@
+package com.fs;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
+import org.springframework.scheduling.annotation.EnableAsync;
+import org.springframework.scheduling.annotation.EnableScheduling;
+
+/**
+ * 启动程序
+ */
+@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
+@EnableAsync
+@EnableScheduling
+public class FsAdNewApiApplication {
+    public static void main(String[] args) {
+        SpringApplication.run(FsAdNewApiApplication.class, args);
+        System.out.println("========================================");
+        System.out.println("ad-new-api启动成功");
+        System.out.println("========================================");
+    }
+}

+ 130 - 0
fs-ad-new-api/src/main/java/com/fs/app/controller/CallbackController.java

@@ -0,0 +1,130 @@
+package com.fs.app.controller;
+
+import cn.hutool.core.util.ObjectUtil;
+import com.fs.newAdv.integration.client.IAccessTokenClient;
+import com.fs.newAdv.integration.client.IApiClient;
+import com.fs.newAdv.integration.factory.AdvertiserHandlerFactory;
+import com.fs.common.result.Result;
+import com.fs.newAdv.domain.PromotionAccount;
+import com.fs.newAdv.enums.AdvertiserTypeEnum;
+import com.fs.newAdv.service.IPromotionAccountService;
+import com.fs.newAdv.vo.AccessTokenByAuthCodeVo;
+import com.fs.newAdv.vo.AccessTokenVo;
+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.RestController;
+
+import javax.servlet.http.HttpServletRequest;
+import java.time.LocalDateTime;
+
+/**
+ * 广告商回调接口
+ */
+@Slf4j
+@RestController
+@RequestMapping("/callBack")
+public class CallbackController {
+
+    @Autowired
+    private AdvertiserHandlerFactory advertiserHandlerFactory;
+    @Autowired
+    private IPromotionAccountService promotionAccountService;
+
+    /**
+     * 百度回调接口
+     *
+     * @param request
+     * @return
+     */
+    @GetMapping("/baidu/getAuthCode")
+    public Result<String> getBaiduAuthCode(HttpServletRequest request) {
+        getAccessToken(AdvertiserTypeEnum.BAIDU, request.getParameter("state"), request.getParameter("authCode"));
+        return Result.success("");
+    }
+
+    /**
+     * 巨量回调接口
+     *
+     * @param request
+     * @return
+     */
+    @GetMapping("/oceanEngine/getAuthCode")
+    public Result<String> getOceanEngineAuthCode(HttpServletRequest request) {
+        getAccessToken(AdvertiserTypeEnum.OCEANENGINE, request.getParameter("state"), request.getParameter("auth_code"));
+        return Result.success("");
+    }
+
+    /**
+     * 腾讯回调接口
+     *
+     * @param request
+     * @return
+     */
+    @GetMapping("/tencent/getAuthCode")
+    public Result<String> getTencentTAuthCode(HttpServletRequest request) {
+        getAccessToken(AdvertiserTypeEnum.TENCENT, request.getParameter("state"), request.getParameter("authorization_code"));
+        return Result.success("");
+    }
+
+    /**
+     * OPPO回调接口
+     *
+     * @param request
+     * @return
+     */
+    @GetMapping("/oppo/getAuthCode")
+    public Result<String> getOppoAuthCode(HttpServletRequest request) {
+        getAccessToken(AdvertiserTypeEnum.OCEANENGINE, request.getParameter("state"), request.getParameter("auth_code"));
+        return Result.success("");
+    }
+
+    /**
+     * vivo回调接口
+     *
+     * @param request
+     * @return
+     */
+    @GetMapping("/vivo/getAuthCode")
+    public Result<String> getViVoAuthCode(HttpServletRequest request) {
+        getAccessToken(AdvertiserTypeEnum.OCEANENGINE, request.getParameter("state"), request.getParameter("auth_code"));
+        return Result.success("");
+    }
+
+    /**
+     * iqiyi回调接口
+     *
+     * @param request
+     * @return
+     */
+    @GetMapping("/iqiyi/getAuthCode")
+    public Result<String> getiqiyiAuthCode(HttpServletRequest request) {
+        getAccessToken(AdvertiserTypeEnum.OCEANENGINE, request.getParameter("state"), request.getParameter("auth_code"));
+        return Result.success("");
+    }
+
+    private void getAccessToken(AdvertiserTypeEnum advertiserType, String state, String authCode) {
+        PromotionAccount byId = promotionAccountService.getById(state);
+        // 获取请求参数
+        IApiClient apiClient = advertiserHandlerFactory.getApiClient(advertiserType);
+        IAccessTokenClient tokenClient = (IAccessTokenClient) apiClient;
+        AccessTokenVo accessToken = tokenClient.getAccessTokenByAuthCode(AccessTokenByAuthCodeVo
+                .builder()
+                .adAccountId(byId.getAdAccountId())
+                .appId(byId.getAppId())
+                .authCode(authCode)
+                .appSecret(byId.getAppSecret())
+                .appSecret(byId.getAppSecret())
+                .build());
+        if (ObjectUtil.isNotEmpty(accessToken)) {
+            byId.setAccessToken(accessToken.getAccessToken());
+            byId.setRefreshToken(accessToken.getRefreshToken());
+            byId.setExpireTime(accessToken.getExpireTime());
+            byId.setUpdateTime(LocalDateTime.now());
+            promotionAccountService.updateById(byId);
+        } else {
+            log.error("获取accessToken失败:{}", byId.getId());
+        }
+    }
+}

+ 51 - 0
fs-ad-new-api/src/main/java/com/fs/app/controller/LandingPageController.java

@@ -0,0 +1,51 @@
+package com.fs.app.controller;
+
+import com.fs.app.facade.CallbackProcessingFacadeService;
+import com.fs.common.result.Result;
+import com.fs.newAdv.dto.req.LandingIndexReq;
+import com.fs.newAdv.dto.req.WeChatLandingIndexReq;
+import com.fs.newAdv.dto.res.LandingIndexRes;
+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;
+
+/**
+ * 落地页控制器
+ * 用于接收广告平台跳转时携带的参数
+ *
+ * @author zhangqin
+ * @date 2025-11-04
+ */
+@Slf4j
+@RestController
+@RequestMapping("/landing")
+public class LandingPageController {
+
+    @Autowired
+    private CallbackProcessingFacadeService facadeService;
+
+    /**
+     * 落地页访问
+     */
+    @PostMapping("/h5/home")
+    public Result<LandingIndexRes> track(
+            @RequestBody LandingIndexReq req) {
+        log.info("落地页访问追踪:req={}", req);
+        // 查询落地页模板
+        return Result.success(facadeService.getLandingIndexBySiteId(req.getAllParams()));
+    }
+
+    /**
+     * 小程序访问
+     */
+    @PostMapping("/mini/home")
+    public Result<LandingIndexRes> track(@RequestBody WeChatLandingIndexReq req) {
+        return Result.success(facadeService.getWxLandingIndexBySiteId(req));
+    }
+
+
+}
+

+ 42 - 0
fs-ad-new-api/src/main/java/com/fs/app/controller/TestController.java

@@ -0,0 +1,42 @@
+package com.fs.app.controller;
+
+import com.fs.newAdv.integration.client.advertiser.BaiduApiClient;
+import com.fs.newAdv.enums.SystemEventTypeEnum;
+import com.fs.newAdv.event.ConversionEventPublisher;
+import com.fs.newAdv.service.IPromotionAccountService;
+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.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * 广告商监测链接
+ *
+ * @author zhangqin
+ */
+@Slf4j
+@RestController
+@RequestMapping("/test")
+public class TestController {
+
+    @Autowired
+    private ConversionEventPublisher conversionEventPublisher;
+    @Autowired
+    private BaiduApiClient baiduApiClient;
+    @Autowired
+    private IPromotionAccountService promotionAccountService;
+
+    @GetMapping("/test1/{traceId}")
+    public void test1(@PathVariable("traceId") String traceId) {
+        log.info("模拟 当日加群 事件完成");
+        conversionEventPublisher.publishConversionEvent(traceId, SystemEventTypeEnum.GROUP_TODAY);
+    }
+
+    @GetMapping("/test2/{traceId}")
+    public void test2(@PathVariable("traceId") String traceId) {
+        log.info("模拟 当日加微 事件完成");
+        conversionEventPublisher.publishConversionEvent(traceId, SystemEventTypeEnum.WEI_CHAT_TODAY);
+    }
+}

+ 48 - 0
fs-ad-new-api/src/main/java/com/fs/app/controller/TrackingController.java

@@ -0,0 +1,48 @@
+package com.fs.app.controller;
+
+import com.fs.app.facade.CallbackProcessingFacadeService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import javax.servlet.http.HttpServletResponse;
+import java.util.Map;
+
+/**
+ * 广告商监测链接
+ *
+ * @author zhangqin
+ */
+@Slf4j
+@RestController
+@RequestMapping("/track")
+public class TrackingController {
+
+    @Autowired
+    private CallbackProcessingFacadeService facadeService;
+
+
+    /**
+     * 监测链接端口
+     *
+     * @param allParams 所有URL参数
+     * @param response  HTTP响应
+     */
+    @GetMapping("/click/{advertiserCode}")
+    public void trackBaidu(
+            @RequestParam Map<String, String> allParams,
+            @PathVariable("advertiserCode") Long advertiserCode,
+            HttpServletResponse response) {
+        log.info("接收监测请求 | params={}", allParams);
+        try {
+            // 2. 保存点击追踪记录
+            facadeService.saveClickTrace(advertiserCode,allParams);
+            // 3. 返回 200 OK
+            response.setStatus(HttpServletResponse.SC_OK);
+        } catch (Exception e) {
+            log.error("监测请求处理失败 | params={}", allParams, e);
+            response.setStatus(HttpServletResponse.SC_OK);
+        }
+    }
+
+}

+ 88 - 0
fs-ad-new-api/src/main/java/com/fs/app/controller/WeChatController.java

@@ -0,0 +1,88 @@
+package com.fs.app.controller;
+
+import cn.hutool.http.HttpRequest;
+import cn.hutool.http.HttpResponse;
+import cn.hutool.json.JSONUtil;
+import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.fs.app.facade.CallbackProcessingFacadeService;
+import com.fs.common.constant.SystemConstant;
+import com.fs.common.result.Result;
+import com.fs.newAdv.domain.AdvMiniConfig;
+import com.fs.newAdv.service.IAdvMiniConfigService;
+import com.fs.wx.miniapp.config.WxMaProperties;
+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.time.LocalDateTime;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 广告商监测链接
+ *
+ * @author zhangqin
+ */
+@Slf4j
+@RestController
+@RequestMapping("/scheme")
+public class WeChatController {
+
+    @Autowired
+    private CallbackProcessingFacadeService facadeService;
+
+    @Autowired
+    private WxMaProperties properties;
+
+    @Autowired
+    private IAdvMiniConfigService advMiniConfigService;
+
+    @GetMapping("/getSchemeUrl")
+    public Result<String> getSchemeUrl(@RequestParam(value = "traceId") String traceId) {
+        List<AdvMiniConfig> list = advMiniConfigService.list(new LambdaQueryWrapper<AdvMiniConfig>()
+                .eq(AdvMiniConfig::getStatus, 1));
+        for (AdvMiniConfig advMiniConfig : list) {
+            try {
+                String access_token = advMiniConfig.getAccessToken();
+                // 判断token是否过期
+                if (advMiniConfig.getExpiresIn().isBefore(LocalDateTime.now().plusMinutes(10))) {
+                    HttpResponse execute2 = HttpRequest.get("https://api.weixin.qq.com/cgi-bin/token")
+                            .form("grant_type", "client_credential")
+                            .form("appid", advMiniConfig.getAppId())
+                            .form("secret", advMiniConfig.getAppSecret())
+                            .timeout(SystemConstant.API_TIMEOUT)
+                            .execute();
+                    JSONObject obj = JSONObject.parseObject(execute2.body());
+                    access_token = obj.getString("access_token");
+                    advMiniConfig.setAccessToken(access_token);
+                    advMiniConfig.setExpiresIn(LocalDateTime.now().plusSeconds(obj.getLong("expires_in")));
+                    advMiniConfigService.updateById(advMiniConfig);
+                }
+                Map<String, Object> map = new HashMap<>();
+                Map<String, Object> map2 = new HashMap<>();
+                map2.put("path", "/pages/shopping/productDetails");
+                map2.put("query", "traceId=" + traceId);
+                map2.put("env_version", "trial");
+                map.put("jump_wxa", map2);
+                map.put("is_expire", false);
+                HttpResponse execute = HttpRequest.post("https://api.weixin.qq.com/wxa/generatescheme?access_token=" + access_token)
+                        .header("Content-Type", "application/json")
+                        .body(JSONUtil.toJsonStr(map))
+                        .timeout(SystemConstant.API_TIMEOUT)
+                        .execute();
+                log.info("getSchemeUrl:{}", execute.body());
+                JSONObject jsonObject = JSONObject.parseObject(execute.body());
+                //response.addHeader("Access-Control-Allow-Origin", "*");
+                return Result.success(jsonObject.getString("openlink"));
+            }catch (Exception e){
+                log.error("getSchemeUrl error:{}",advMiniConfig.getAppId(), e);
+            }
+        }
+        return Result.success("");
+    }
+}

+ 29 - 0
fs-ad-new-api/src/main/java/com/fs/app/facade/CallbackProcessingFacadeService.java

@@ -0,0 +1,29 @@
+package com.fs.app.facade;
+
+import com.fs.newAdv.dto.req.WeChatLandingIndexReq;
+import com.fs.newAdv.dto.res.LandingIndexRes;
+
+import java.util.Map;
+
+public interface CallbackProcessingFacadeService {
+
+
+    /**
+     * 链化追踪点击
+     * @param allParams
+     */
+    void saveClickTrace(Long advertiserCode,Map<String,String> allParams);
+    /**
+     * 根据站点ID获取落地页信息
+     *
+     * @param siteId
+     * @return
+     */
+    LandingIndexRes getLandingIndexBySiteId(Map<String, String> allParams);
+
+    //----------------------code回调---------------------------------
+    void gdtGetAuthCode(Integer code, Long state);
+
+
+    LandingIndexRes getWxLandingIndexBySiteId(WeChatLandingIndexReq req);
+}

+ 294 - 0
fs-ad-new-api/src/main/java/com/fs/app/facade/CallbackProcessingFacadeServiceImpl.java

@@ -0,0 +1,294 @@
+package com.fs.app.facade;
+
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.json.JSONArray;
+import cn.hutool.json.JSONObject;
+import cn.hutool.json.JSONUtil;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
+import com.fs.newAdv.integration.adapter.IAdvertiserAdapter;
+import com.fs.newAdv.integration.factory.AdvertiserHandlerFactory;
+import com.fs.common.exception.base.BusinessException;
+import com.fs.common.utils.RedisUtil;
+import com.fs.newAdv.domain.LandingPageTemplate;
+import com.fs.newAdv.domain.Lead;
+import com.fs.newAdv.domain.Site;
+import com.fs.newAdv.dto.req.WeChatLandingIndexReq;
+import com.fs.newAdv.dto.res.LandingIndexRes;
+import com.fs.newAdv.enums.AdvertiserTypeEnum;
+import com.fs.newAdv.service.ILandingPageTemplateService;
+import com.fs.newAdv.service.ILeadService;
+import com.fs.newAdv.service.ISiteService;
+import com.fs.qw.domain.QwAssignRule;
+import com.fs.qw.domain.QwAssignRuleUser;
+import com.fs.qw.domain.QwContactWay;
+import com.fs.qw.domain.QwGroupActual;
+import com.fs.qw.service.IQwAssignRuleService;
+import com.fs.qw.service.IQwAssignRuleUserService;
+import com.fs.qw.service.IQwContactWayService;
+import com.fs.qw.service.IQwGroupActualService;
+import com.fs.qwApi.Result.QwAddContactWayResult;
+import com.fs.qwApi.param.QwAddContactWayParam;
+import com.fs.qwApi.service.QwApiService;
+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.time.LocalDateTime;
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+@Service
+@Slf4j
+public class CallbackProcessingFacadeServiceImpl implements CallbackProcessingFacadeService {
+    @Autowired
+    private AdvertiserHandlerFactory handlerFactory;
+    @Autowired
+    private ILeadService leadService;
+    @Autowired
+    private ISiteService siteService;
+    @Autowired
+    private IQwAssignRuleUserService assignRuleUserService;
+    @Autowired
+    private IQwAssignRuleService assignRuleService;
+    @Autowired
+    private ILandingPageTemplateService landingPageTemplateService;
+    @Autowired
+    private IQwGroupActualService actualService;
+    @Autowired
+    private QwApiService qwApiService;
+    @Autowired
+    private IQwContactWayService contactWayService;
+
+    @Autowired
+    private RedisUtil redisUtil;
+
+    private static final String TEMPLATE_DATA = "new-adv:template-data:";
+
+    @Override
+    public void saveClickTrace(Long advertiserCode, Map<String, String> allParams) {
+        IAdvertiserAdapter advertiserAdapter = handlerFactory.getAdapter(AdvertiserTypeEnum.getByCode(advertiserCode));
+        Lead lead = advertiserAdapter.adaptCallbackData(allParams);
+        Lead byTraceId = leadService.getByTraceId(lead.getTraceId());
+        if (ObjectUtil.isNotEmpty(byTraceId)) {
+            throw new BusinessException("监测信息已存在: " + lead.getTraceId());
+        }
+        lead.setStatus(0);
+        lead.setClickTrigger(1);
+        lead.setTraceRawParams(JSONUtil.toJsonStr(allParams));
+        boolean saved = leadService.save(lead);
+        if (!saved) {
+            log.error("线索保存失败:{}", lead);
+            throw new RuntimeException("线索保存失败");
+        }
+    }
+
+    private String getTraceIdByAdvertiser(AdvertiserTypeEnum byCode, Map<String, String> allParams) {
+        String traceId;
+        switch (byCode) {
+            case OCEANENGINE:
+            case TENCENT:
+            case OPPO:
+                traceId = allParams.get("click_id");
+                break;
+            case BAIDU:
+                traceId = allParams.get("bd_vid");
+                break;
+            case VIVO:
+                traceId = allParams.get("requestId");
+                break;
+            case IQIYI:
+                traceId = allParams.get("traceId");
+                break;
+            default:
+                traceId = "ylrz_test";
+        }
+        if (StrUtil.isEmpty(traceId)) {
+            traceId = "ylrz_test";
+        }
+
+        return traceId;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public LandingIndexRes getLandingIndexBySiteId(Map<String, String> allParams) {
+        // 站点信息
+        String paramsSiteId = allParams.get("siteId");
+        if (ObjectUtil.isEmpty(paramsSiteId)) {
+            log.info("站点id不存在:{}", paramsSiteId);
+            return null;
+        }
+        Long siteId = Long.valueOf(paramsSiteId);
+        Site byId = siteService.getById(siteId);
+        if (ObjectUtil.isEmpty(byId)) {
+            log.info("站点信息不存在:{}", siteId);
+            return null;
+        }
+        // 广告商信息
+        Long advertiserId = byId.getAdvertiserId();
+        // 访问链路id
+        String traceId = getTraceIdByAdvertiser(Objects.requireNonNull(AdvertiserTypeEnum.getByCode(advertiserId)), allParams);
+        // 线索信息
+        Lead byTraceId = leadService.getByTraceId(traceId);
+        boolean isNewLead = ObjectUtil.isEmpty(byTraceId);
+        if (isNewLead) {
+            IAdvertiserAdapter advertiserAdapter = handlerFactory.getAdapter(AdvertiserTypeEnum.getByCode(advertiserId));
+            byTraceId = advertiserAdapter.adaptCallbackData(allParams);
+            byTraceId.setAdvertiserId(advertiserId);
+            byTraceId.setSiteId(siteId);
+            byTraceId.setTraceId(traceId);
+            // 设置站点和落地页的关联
+            setSiteByIdeaId(siteId, byTraceId.getIdeaId());
+        } else {
+            // 检查站点和广告商信息是否异常
+            if (!Objects.equals(byTraceId.getSiteId(), siteId)) {
+                log.info("落地页站点信息异常:{}---{}", byTraceId.getSiteId(), siteId);
+            }
+            if (!Objects.equals(byTraceId.getAdvertiserId(), advertiserId)) {
+                log.info("落地页广告商信息异常:{}---{}", byTraceId.getAdvertiserId(), advertiserId);
+            }
+        }
+        // 模板缓存
+/*        Object ca = redisUtil.get(TEMPLATE_DATA + traceId);
+        String templateData;
+        if (ca != null) {
+            templateData = String.valueOf(ca);
+        } else {
+            // 查询模板数据
+            LandingPageTemplate landingPageTemplate = landingPageTemplateService.getById(byId.getLaunchPageId());
+            JSONObject jsonObject = JSONUtil.parseObj(landingPageTemplate.getTemplateData());
+            // 替换二维码链接
+            updateQrCodeInTemplate(jsonObject, traceId, byId, byTraceId);
+            templateData = JSONUtil.toJsonStr(jsonObject);
+        }*/
+
+        // 查询模板数据
+        LandingPageTemplate landingPageTemplate = landingPageTemplateService.getById(byId.getLaunchPageId());
+        JSONObject jsonObject = JSONUtil.parseObj(landingPageTemplate.getTemplateData());
+        // 替换二维码链接
+        updateQrCodeInTemplate(jsonObject, traceId, byId, byTraceId);
+        String templateData = JSONUtil.toJsonStr(jsonObject);
+
+        // 保存或更新 线索信息
+        LocalDateTime now = LocalDateTime.now();
+        byTraceId.setLandingPageRawParams(JSONUtil.toJsonStr(allParams));
+        byTraceId.setLandingPageTrigger(1);
+        byTraceId.setLandingPageTs(now);
+        byTraceId.setUpdateTime(now);
+        if (isNewLead) {
+            leadService.save(byTraceId);
+        } else {
+            leadService.updateById(byTraceId);
+        }
+
+        // 封装返回结果
+        LandingIndexRes res = new LandingIndexRes();
+        redisUtil.set(TEMPLATE_DATA + traceId, templateData, 24, TimeUnit.HOURS);
+        res.setTemplateData(templateData);
+        res.setTraceId(byTraceId.getTraceId());
+        return res;
+    }
+
+    private void setSiteByIdeaId(Long siteId, String ideaId) {
+        siteService.update(new LambdaUpdateWrapper<Site>()
+                .eq(Site::getId, siteId)
+                .set(Site::getIdeaId, ideaId));
+    }
+
+    /**
+     * 更新模板中的二维码信息
+     */
+    private void updateQrCodeInTemplate(JSONObject templateData, String traceId, Site site, Lead lead) {
+        JSONArray configList = templateData.getJSONArray("configList");
+        if (configList == null || configList.isEmpty()) {
+            return;
+        }
+        AtomicReference<String> qrCode = new AtomicReference<>("");
+        configList.stream()
+                .map(config -> (JSONObject) config)
+                .map(config -> config.getJSONArray("moduleList"))
+                .filter(ObjectUtil::isNotEmpty)
+                .flatMap(Collection::stream)
+                .map(module -> (JSONObject) module)
+                .filter(module -> "h5-qrcode".equals(module.getStr("type")))
+                .forEach(module -> {
+                    if (StrUtil.isEmpty(qrCode.get())) {
+                        qrCode.set(getQrCodeByAllocationRuleId(site.getLaunchType(), site.getAllocationRule(), site.getAllocationRuleId(), lead));
+                    }
+                    module.set("workUrl", qrCode.get());
+                });
+    }
+
+    private String getQrCodeByAllocationRuleId(Integer launchType,
+                                               Integer allocationRule,
+                                               Long allocationRuleId,
+                                               Lead byTraceId) {
+        log.info("开始获取广告二维码: {} {} {} {}", launchType, allocationRule, allocationRuleId ,byTraceId);
+        // 二维码
+        String qrCode = "";
+        if (allocationRule == 1) {
+            // 个人分配规则
+            QwAssignRule byId1 = assignRuleService.getById(allocationRuleId);
+            QwAssignRuleUser qwAssignRuleUser = assignRuleUserService.getUserByAllocationRuleId(allocationRuleId, byId1.getAssignType());
+            byTraceId.setQwAssignRuleUserId(qwAssignRuleUser.getId());
+            if (byId1.getAssignType().equals(3)) {
+                List<String> users = Collections.singletonList(qwAssignRuleUser.getQwUserId());
+                QwAddContactWayParam qwAddContactWayParam = new QwAddContactWayParam();
+                qwAddContactWayParam.setType(1);
+                qwAddContactWayParam.setScene(2);
+                qwAddContactWayParam.setUser(users);
+                qwAddContactWayParam.setSkip_verify(true);
+                QwAddContactWayResult qwAddContactWayResult = qwApiService.addContactWay(qwAddContactWayParam, qwAssignRuleUser.getCorpId());
+                qrCode = qwAddContactWayResult.getQr_code();
+            }
+        } else if (allocationRule == 2) {
+            // 活码分配规则
+            if (launchType.equals(1)) {
+                // 线上规则--群活码
+                QwGroupActual qwGroupActual = actualService.getGroupActualByAllocationRuleId(allocationRuleId);
+                byTraceId.setQwGroupActualId(qwGroupActual.getId());
+                byTraceId.setQwGroupLiveCodeId(qwGroupActual.getLiveCodeId());
+                qrCode = qwGroupActual.getGroupUrl();
+            } else if (launchType.equals(2)) {
+                // 线下规则--个人活码
+                QwContactWay qwContactWay = contactWayService.selectQwContactWayById(allocationRuleId);
+                byTraceId.setQwContactWayId(qwContactWay.getId());
+                qrCode = qwContactWay.getQrCode();
+            }
+        }
+        log.info("落地页广告获取二维码: {}", qrCode);
+        return qrCode;
+    }
+
+    @Override
+    public void gdtGetAuthCode(Integer code, Long state) {
+   /*     if (code == null || state == null) {
+            return;
+        }
+        PromotionAccount byId = promotionAccountService.getById(state);
+        if (byId == null) {
+            return;
+        }
+        IApiClient apiClient = handlerFactory.getApiClient(AdvertiserTypeEnum.TENCENT);
+        if (apiClient instanceof IAccessTokenClient) {
+            IAccessTokenClient tokenClient = (IAccessTokenClient) apiClient;
+            Map<String, String> accessToken = tokenClient.getAccessToken(code, byId.getAppId(), byId.getAppSecret(), byId.getCallbackUrl());
+            byId.setAccessToken(accessToken.get("access_token"));
+            byId.setRefreshToken(accessToken.get("refresh_token"));
+        }*/
+    }
+
+    @Override
+    public LandingIndexRes getWxLandingIndexBySiteId(WeChatLandingIndexReq req) {
+        // 更新授权页访问信息记录
+        leadService.updateAuthIndex(req.getTraceId(),req.getType());
+        String templateData = String.valueOf(redisUtil.get(TEMPLATE_DATA + req.getTraceId()));
+        LandingIndexRes landingIndexRes = new LandingIndexRes();
+        landingIndexRes.setTemplateData(templateData);
+        return landingIndexRes;
+    }
+}

+ 172 - 0
fs-ad-new-api/src/main/java/com/fs/app/facade/ConversionServiceImpl.java

@@ -0,0 +1,172 @@
+package com.fs.app.facade;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.json.JSONUtil;
+import com.fs.newAdv.integration.adapter.IAdvertiserAdapter;
+import com.fs.newAdv.integration.client.IApiClient;
+import com.fs.newAdv.integration.factory.AdvertiserHandlerFactory;
+import com.fs.common.utils.RedisUtil;
+import com.fs.common.utils.SnowflakeUtil;
+import com.fs.newAdv.domain.*;
+import com.fs.newAdv.enums.AdvertiserTypeEnum;
+import com.fs.newAdv.enums.SystemEventTypeEnum;
+import com.fs.newAdv.mapper.ConversionLogMapper;
+import com.fs.newAdv.mapper.ConversionTargetMapper;
+import com.fs.newAdv.service.ICallbackAccountService;
+import com.fs.newAdv.service.ILeadService;
+import com.fs.newAdv.service.IPromotionAccountService;
+import com.fs.newAdv.service.ISiteService;
+import com.fs.newAdv.vo.ConversionEventVo;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 转化回传服务实现类
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ */
+@Slf4j
+@Service
+public class ConversionServiceImpl implements IConversionService {
+
+    @Autowired
+    private ISiteService siteService;
+    @Autowired
+    private ICallbackAccountService callbackAccountService;
+    @Autowired
+    private IPromotionAccountService promotionAccountService;
+
+
+    @Autowired
+    private ConversionLogMapper conversionLogMapper;
+
+    @Autowired
+    private ConversionTargetMapper conversionTargetMapper;
+
+    @Autowired
+    private AdvertiserHandlerFactory advertiserHandlerFactory;
+
+    @Autowired
+    private RedisUtil redisUtil;
+
+    @Autowired
+    private ILeadService leadService;
+
+    @Override
+    public boolean reportTrackingToAdvertiser(SystemEventTypeEnum systemEventTypeEnum, String traceId) {
+        // 线索信息
+        Lead lead = leadService.getByTraceId(traceId);
+        // 站点信息
+        Site site = siteService.getById(lead.getSiteId());
+        if (site == null) {
+            log.error("站点不存在:{}", site);
+            return false;
+        }
+        // 查询回传信息
+        CallbackAccount callbackAccount = callbackAccountService.getById(site.getCallbackAccountId());
+        // 回传事件
+        List<ConversionEventVo> conversionEventVo = callbackAccount.getConversionEventVo();
+        if (CollUtil.isEmpty(conversionEventVo)) {
+            log.error("回传事件不存在: {}", traceId);
+            return false;
+        }
+
+        ConversionEventVo advertiserEventType = conversionEventVo.stream()
+                .filter(item -> item.getSystemEventType().equals(systemEventTypeEnum.getCode()))
+                .findFirst()
+                .orElse(null);
+
+        if (ObjectUtil.isEmpty(advertiserEventType)) {
+            log.error("回传事件未匹配: {}", traceId);
+            return false;
+        }
+
+        // 查询广告商账号
+        PromotionAccount promotionAccount = promotionAccountService.getById(site.getPromotionAccountId());
+        if (promotionAccount == null) {
+            log.error("回传账号不存在:{}", site.getPromotionAccountId());
+            return false;
+        }
+
+        Map<String, Object> params = JSONUtil.toBean(lead.getTraceRawParams(), Map.class);
+        // 构建回传参数
+        Map<String, Object> conversionData = new HashMap<>();
+        // ------------------------------通用参数----------
+        conversionData.put("adAccountId", callbackAccount.getAdAccountId());
+        conversionData.put("appId", callbackAccount.getAppId());
+        conversionData.put("appSecret", callbackAccount.getAppSecret());
+        conversionData.put("accessToken", callbackAccount.getAccessToken());
+        conversionData.put("ip", lead.getIp());
+        conversionData.put("scr_id", callbackAccount.getScrId());
+        conversionData.put("traceId", traceId);
+        conversionData.put("eventType", advertiserEventType.getAdvertiserEventType());
+        conversionData.put("timestamp", System.currentTimeMillis() / 1000);
+        // 添加平台适应参数
+        IAdvertiserAdapter adapter = advertiserHandlerFactory.getAdapter(AdvertiserTypeEnum.getByCode(callbackAccount.getAdvertiserId()));
+        adapter.uploadConversionData(conversionData, params);
+
+
+        // 调用API回传
+        IApiClient apiClient = advertiserHandlerFactory.getApiClient(AdvertiserTypeEnum.getByCode(site.getAdvertiserId()));
+        boolean b = apiClient.reportConversion(conversionData);
+
+        // 保存转化日志(待回传状态)
+        ConversionLog conversionLog = saveConversionLog(
+                site.getCallbackAccountId(),
+                site.getId(), site.getAdvertiserId(), site.getAdvertiserName(),
+                advertiserEventType, traceId, conversionData, b);
+        return b;
+    }
+
+
+    /**
+     * 构建巨量引擎转化数据
+     */
+    private Map<String, Object> buildOceanEngineConversionData(
+            String clickId, String eventType, Double value) {
+
+  /*      Map<String, Object> data = new HashMap<>();
+        data.put("clickId", clickId);
+        data.put("eventType", mapEventType(eventType));
+
+        if (value != null && value > 0) {
+            data.put("value", value);
+        }*/
+
+        return null;
+    }
+
+    /**
+     * 保存转化日志
+     */
+    private ConversionLog saveConversionLog(Long callbackAccountId,Long siteId, Long advertiserId,
+                                            String advertiserName, ConversionEventVo eventType,
+                                            String clickId,
+                                            Map<String, Object> conversionData,
+                                            boolean status) {
+        ConversionLog log = new ConversionLog();
+        log.setId(SnowflakeUtil.nextId());
+        log.setCallbackAccountId(callbackAccountId);
+        log.setSiteId(siteId);
+        log.setAdvertiserId(advertiserId);
+        log.setAdvertiserName(advertiserName);
+        log.setSysConversionEvent(eventType.getSystemEventTypeName());
+        log.setAdvConversionEvent(eventType.getAdvertiserEventName());
+        log.setCallbackParams(JSONUtil.toJsonStr(conversionData));
+        log.setCallbackStatus(status ? 1 : 2);
+        log.setRetryCount(0);
+        log.setTraceId(clickId);
+        log.setCreateTime(LocalDateTime.now());
+        conversionLogMapper.insert(log);
+        return log;
+    }
+}
+

+ 23 - 0
fs-ad-new-api/src/main/java/com/fs/app/facade/IConversionService.java

@@ -0,0 +1,23 @@
+package com.fs.app.facade;
+
+
+import com.fs.newAdv.enums.SystemEventTypeEnum;
+
+/**
+ * 转化回传服务接口
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ * @updated 2025-11-05 使用clickId参数直接回传
+ */
+public interface IConversionService {
+    /**
+     * 链化追踪回传转化数据到广告商
+     *
+     * @param eventType 事件类型
+     * @param traceId   点击ID(广告平台提供)
+     * @return 是否成功
+     */
+    boolean reportTrackingToAdvertiser(SystemEventTypeEnum eventType, String traceId);
+}
+

+ 71 - 0
fs-ad-new-api/src/main/java/com/fs/app/mq/consumer/ConversionTrackingMessageConsumer.java

@@ -0,0 +1,71 @@
+package com.fs.app.mq.consumer;
+
+import com.fs.app.facade.IConversionService;
+import com.fs.common.annotation.DistributeLock;
+import com.fs.common.utils.RedisUtil;
+import com.fs.common.utils.TraceIdUtil;
+import com.fs.newAdv.constant.ConversionTrackingMessage;
+import com.fs.newAdv.constant.MqTopicConstant;
+import com.fs.newAdv.enums.SystemEventTypeEnum;
+import com.fs.newAdv.mapper.ConversionLogMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.rocketmq.spring.annotation.ConsumeMode;
+import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
+import org.apache.rocketmq.spring.core.RocketMQListener;
+import org.jboss.logging.MDC;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+/**
+ * 转化追踪
+ *
+ * @author zhangqin
+ */
+@Slf4j
+@Component
+@RocketMQMessageListener(
+        topic = MqTopicConstant.CONVERSION_TRACKING_TOPIC,
+        consumerGroup = MqTopicConstant.CONVERSION_TRACKING_TOPIC_CONSUMER_GROUP,
+        // 并发消费模式(多线程并发消费,线程数由RocketMQ自动管理)
+        consumeMode = ConsumeMode.CONCURRENTLY,
+        // 最大重试次数(RocketMQ默认16次)
+        maxReconsumeTimes = 16
+)
+public class ConversionTrackingMessageConsumer implements RocketMQListener<ConversionTrackingMessage> {
+
+    @Autowired
+    private IConversionService conversionService;
+
+    @Autowired
+    private RedisUtil redisUtil;
+
+    @Autowired
+    private ConversionLogMapper conversionLogMapper;
+
+    /**
+     * 消费转化消息
+     *
+     * @param message 转化消息
+     */
+    @Override
+    @DistributeLock(scene = "mq", keyExpression = "#message.traceId", waitTime = 0, errorMsg = "重复消费")
+    public void onMessage(ConversionTrackingMessage message) {
+        TraceIdUtil.put(message.getTrackId());
+
+        String traceId = message.getTraceId();
+        SystemEventTypeEnum eventType = message.getEventType();
+
+        log.info("转化追踪消息开始消费 | traceId={}, eventType={}",
+                traceId, eventType.getDescription());
+
+        //  调用广告平台API回传
+        boolean success = conversionService.reportTrackingToAdvertiser(eventType, traceId);
+
+        if (success) {
+            log.info("转化回传成功 | traceId={}, eventType={}", traceId, eventType);
+        } else {
+            log.info("转化回传失败 | traceId={}, eventType={}", traceId, eventType);
+        }
+    }
+}
+

+ 91 - 0
fs-ad-new-api/src/main/java/com/fs/app/task/ConversionRetryTask.java

@@ -0,0 +1,91 @@
+package com.fs.app.task;
+
+
+import cn.hutool.json.JSONUtil;
+import com.fs.common.annotation.DistributeLock;
+import com.fs.common.constant.SystemConstant;
+import com.fs.newAdv.domain.ConversionLog;
+import com.fs.newAdv.enums.AdvertiserTypeEnum;
+import com.fs.newAdv.integration.client.IApiClient;
+import com.fs.newAdv.integration.factory.AdvertiserHandlerFactory;
+import com.fs.newAdv.mapper.ConversionLogMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * 转化回传重试定时任务
+ * 每10分钟执行一次,重试失败的转化回传
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ */
+@Slf4j
+@Component
+public class ConversionRetryTask {
+
+    @Autowired
+    private ConversionLogMapper conversionLogMapper;
+    @Autowired
+    private AdvertiserHandlerFactory advertiserHandlerFactory;
+
+    /**
+     * 转化回传重试任务
+     * cron: 每10分钟执行
+     */
+    // @Scheduled(cron = "0 */5 * * * ?")
+    @DistributeLock(scene = "task", key = "conversion_retry", waitTime = 0, errorMsg = "conversion_retry任务已执行")
+    public void execute() {
+        // 查询待重试的转化记录
+        List<ConversionLog> pendingList = conversionLogMapper.selectPendingConversions();
+        log.info("查询到 {} 条待重试的转化记录", pendingList.size());
+
+        int successCount = 0;
+        int failCount = 0;
+
+        for (ConversionLog conversionLog : pendingList) {
+            try {
+                // 重试回传
+                boolean success = retryConversion(conversionLog);
+                if (success) {
+                    successCount++;
+                } else {
+                    failCount++;
+                }
+            } catch (Exception e) {
+                log.error("重试转化回传失败,ID:{}", conversionLog.getId(), e);
+                failCount++;
+            }
+        }
+        log.info("========== 转化回传重试任务完成,成功:{},失败:{} ==========", successCount, failCount);
+    }
+
+    /**
+     * 重试转化回传
+     */
+    private boolean retryConversion(ConversionLog conversionLog) {
+        log.info("重试转化回传,ID:{},重试次数:{}", conversionLog.getId(), conversionLog.getRetryCount());
+        IApiClient apiClient = advertiserHandlerFactory.getApiClient(AdvertiserTypeEnum.getByCode(conversionLog.getAdvertiserId()));
+        boolean b = apiClient.reportConversion(JSONUtil.parseObj(conversionLog.getCallbackParams()));
+        if (b) {
+            conversionLog.setCallbackStatus(1);
+            conversionLog.setSuccessTime(LocalDateTime.now());
+        }
+        // 更新重试次数
+        conversionLog.setRetryCount(conversionLog.getRetryCount() + 1);
+
+        // 如果重试次数超过最大值,标记为失败
+        if (conversionLog.getRetryCount() >= SystemConstant.MAX_RETRY_COUNT) {
+            conversionLog.setCallbackStatus(2); // 失败
+            log.warn("转化回传重试次数已达上限,ID:{}", conversionLog.getId());
+        }
+        conversionLog.setUpdateTime(LocalDateTime.now());
+        conversionLogMapper.updateById(conversionLog);
+        return b;
+    }
+}
+

+ 225 - 0
fs-ad-new-api/src/main/java/com/fs/app/task/DataSyncTask.java

@@ -0,0 +1,225 @@
+package com.fs.app.task;
+
+import cn.hutool.core.date.DateUtil;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.fs.common.annotation.DistributeLock;
+import com.fs.newAdv.domain.Lead;
+import com.fs.newAdv.service.ILeadService;
+import com.fs.newAdv.service.ISiteStatisticsService;
+import com.fs.qw.domain.QwAssignRuleUser;
+import com.fs.qw.domain.QwGroupLiveCode;
+import com.fs.qw.service.IQwAssignRuleUserService;
+import com.fs.qw.service.IQwGroupLiveCodeService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import java.time.LocalDateTime;
+import java.util.Collections;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * 数据同步定时任务
+ * 每2小时执行一次,拉取各平台账户数据
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ */
+@Slf4j
+@Component
+public class DataSyncTask {
+
+
+    @Autowired
+    private ISiteStatisticsService statisticsService;
+    @Autowired
+    private ILeadService leadService;
+    @Autowired
+    private IQwGroupLiveCodeService qwGroupLiveCodeService;
+    @Autowired
+    private IQwAssignRuleUserService qwAssignRuleUserService;
+
+
+    /**
+     * 数据同步任务->同步昨日数据
+     * cron: 每天00:10
+     */
+    @Scheduled(cron = "0 10 0 * * ?")
+    @DistributeLock(scene = "task", key = "sync_yesterday_data", waitTime = 0, errorMsg = "sync_yesterday_data任务已执行")
+    public void syncYesterdayData() {
+        String batchNo = DateUtil.format(LocalDateTime.now().minusDays(1), "yyyy-MM-dd");
+        statisticsService.syncData(batchNo, 1);
+    }
+
+    /**
+     * 数据同步任务->当日数据
+     * cron: 每1小时统计站点数据
+     */
+    @Scheduled(cron = "0 0 0/1 * * ?")
+    @DistributeLock(scene = "task", key = "sync_today_data", waitTime = 0, errorMsg = "sync_today_data任务已执行")
+    public void syncTodayData() throws InterruptedException {
+        String batchNo = DateUtil.format(LocalDateTime.now(), "yyyy-MM-dd");
+        statisticsService.syncData(batchNo, 1);
+    }
+
+
+    /**
+     * 今日加群数据
+     * cron: 每1小时统计站点数据
+     */
+    @Scheduled(cron = "0 0 0/1 * * ?")
+    @DistributeLock(scene = "task", key = "wei_chat_group_to_day_data", waitTime = 0, errorMsg = "wei_chat_group_to_day_data任务已执行")
+    public void weiChatGroupToDayData() {
+        // 统计今日加群数量
+        Optional.ofNullable(leadService.getToDayGroupNum())
+                .orElse(Collections.emptyList())
+                .stream()
+                .map(Lead::getQwGroupLiveCodeId)
+                .filter(Objects::nonNull)
+                .collect(Collectors.groupingBy(
+                        Function.identity(),
+                        Collectors.counting()
+                ))
+                .forEach((k, v) -> {
+                    if (v > 0) {
+                        qwGroupLiveCodeService.update(
+                                new LambdaUpdateWrapper<QwGroupLiveCode>()
+                                        .eq(QwGroupLiveCode::getId, k)
+                                        .set(QwGroupLiveCode::getToDayNum, v)
+                        );
+                    }
+                });
+    }
+
+    /**
+     * 累计加群数据
+     * cron: 每天00:20
+     */
+    @Scheduled(cron = "0 20 0 * * ?")
+    @DistributeLock(scene = "task", key = "wei_chat_group_count_data", waitTime = 0, errorMsg = "wei_chat_group_count_data任务已执行")
+    public void weiChatGroupCountData() {
+        // 统计累积加群数量
+        Optional.ofNullable(leadService.getYesterdayGroupNum())
+                .orElse(Collections.emptyList())
+                .stream()
+                .map(Lead::getQwGroupLiveCodeId)
+                .filter(Objects::nonNull)
+                .collect(Collectors.groupingBy(
+                        Function.identity(),
+                        Collectors.counting()
+                ))
+                .forEach((k, v) -> {
+                    if (v > 0) {
+                        QwGroupLiveCode byId = qwGroupLiveCodeService.getById(k);
+                        byId.setCountNum(byId.getCountNum() + v.intValue());
+                        qwGroupLiveCodeService.updateById(byId);
+                    }
+                });
+    }
+
+
+    /**
+     * 微信当天数据
+     * cron: 一小时执行一次
+     */
+    @Scheduled(cron = "0 0 0/1 * * ?")
+    @DistributeLock(scene = "task", key = "wei_chat_to_day_data", waitTime = 0, errorMsg = "wei_chat_to_day_data任务已执行")
+    public void weiChatToDayData() {
+        // 统计累积加微数量
+        Optional.ofNullable(leadService.getToDayWeiChatNum())
+                .ifPresent(item -> {
+                            // 统计分配数
+                            item.stream()
+                                    .map(Lead::getQwAssignRuleUserId)
+                                    .filter(Objects::nonNull)
+                                    .collect(Collectors.groupingBy(
+                                            Function.identity(),
+                                            Collectors.counting()
+                                    ))
+                                    .forEach((k, v) -> {
+                                        if (v > 0) {
+                                            qwAssignRuleUserService.update(
+                                                    new LambdaUpdateWrapper<QwAssignRuleUser>()
+                                                            .eq(QwAssignRuleUser::getId, k)
+                                                            .set(QwAssignRuleUser::getAssignNumToDay, v)
+                                            );
+                                        }
+                                    });
+                            // 统计添加数
+                            item.stream()
+                                    .filter(data -> data.getAddContactQw().equals(1))
+                                    .map(Lead::getQwAssignRuleUserId)
+                                    .filter(Objects::nonNull)
+                                    .collect(Collectors.groupingBy(
+                                            Function.identity(),
+                                            Collectors.counting()
+                                    ))
+                                    .forEach((k, v) -> {
+                                        if (v > 0) {
+                                            qwAssignRuleUserService.update(
+                                                    new LambdaUpdateWrapper<QwAssignRuleUser>()
+                                                            .eq(QwAssignRuleUser::getId, k)
+                                                            .set(QwAssignRuleUser::getAddNumToDay, v)
+                                            );
+                                        }
+                                    });
+                        }
+                );
+    }
+
+    /**
+     * 微信累计数据
+     * cron: 每天00:30
+     */
+    @Scheduled(cron = "0 30 0 * * ?")
+    @DistributeLock(scene = "task", key = "wei_chat_count_data", waitTime = 0, errorMsg = "wei_chat_count_data任务已执行")
+    public void weiChatCountData() {
+        // 统计累积加微数量
+        Optional.ofNullable(leadService.getYesterdayWeiChatNum())
+                .ifPresent(item -> {
+                            // 统计分配数
+                            item.stream()
+                                    .map(Lead::getQwAssignRuleUserId)
+                                    .filter(Objects::nonNull)
+                                    .collect(Collectors.groupingBy(
+                                            Function.identity(),
+                                            Collectors.counting()
+                                    ))
+                                    .forEach((k, v) -> {
+                                        if (v > 0) {
+                                            QwAssignRuleUser byId = qwAssignRuleUserService.getById(k);
+                                            qwAssignRuleUserService.update(
+                                                    new LambdaUpdateWrapper<QwAssignRuleUser>()
+                                                            .eq(QwAssignRuleUser::getId, k)
+                                                            .set(QwAssignRuleUser::getAssignNumCount, v + byId.getAssignNumCount())
+                                            );
+                                        }
+                                    });
+                            // 统计添加数
+                            item.stream()
+                                    .filter(data -> data.getAddContactQw().equals(1))
+                                    .map(Lead::getQwAssignRuleUserId)
+                                    .filter(Objects::nonNull)
+                                    .collect(Collectors.groupingBy(
+                                            Function.identity(),
+                                            Collectors.counting()
+                                    ))
+                                    .forEach((k, v) -> {
+                                        if (v > 0) {
+                                            QwAssignRuleUser byId = qwAssignRuleUserService.getById(k);
+                                            qwAssignRuleUserService.update(
+                                                    new LambdaUpdateWrapper<QwAssignRuleUser>()
+                                                            .eq(QwAssignRuleUser::getId, k)
+                                                            .set(QwAssignRuleUser::getAddNumCount, v + byId.getAddNumCount())
+                                            );
+                                        }
+                                    });
+                        }
+                );
+    }
+}
+

+ 110 - 0
fs-ad-new-api/src/main/java/com/fs/framework/aspectj/ControllerLogAspect.java

@@ -0,0 +1,110 @@
+
+package com.fs.framework.aspectj;
+
+import com.alibaba.fastjson.JSON;
+import lombok.extern.slf4j.Slf4j;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Pointcut;
+import org.springframework.stereotype.Component;
+import org.springframework.validation.BindingResult;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.Map;
+
+@Slf4j
+@Aspect
+@Component
+public class ControllerLogAspect {
+
+    @Pointcut("execution(* com.fs.app.controller..*.*(..))")
+    public void controllerPointcut() {
+    }
+
+    @Around("controllerPointcut()")
+    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
+        long startTime = System.currentTimeMillis();
+
+        String className = joinPoint.getTarget().getClass().getName();
+        String methodName = joinPoint.getSignature().getName();
+        String fullMethodName = className + "." + methodName;
+
+        Object[] args = joinPoint.getArgs();
+        String requestParams = getRequestParams(args);
+
+        log.info("========== 接口调用开始 ==========");
+        log.info("接口方法: {}", fullMethodName);
+        log.info("请求参数: {}", requestParams);
+
+        Object result = null;
+        try {
+            result = joinPoint.proceed();
+
+            long endTime = System.currentTimeMillis();
+            long costTime = endTime - startTime;
+
+            log.info("返回结果: {}", JSON.toJSONString(result));
+            log.info("接口耗时: {} ms", costTime);
+            log.info("========== 接口调用结束 ==========");
+
+            return result;
+        } catch (Throwable e) {
+            long endTime = System.currentTimeMillis();
+            long costTime = endTime - startTime;
+
+            log.error("接口异常: {}", e.getMessage());
+            log.error("接口耗时: {} ms", costTime);
+            log.error("========== 接口调用异常 ==========");
+
+            throw e;
+        }
+    }
+
+    private String getRequestParams(Object[] args) {
+        if (args == null || args.length == 0) {
+            return "无参数";
+        }
+
+        StringBuilder params = new StringBuilder();
+        for (int i = 0; i < args.length; i++) {
+            if (args[i] != null && !isFilterObject(args[i])) {
+                try {
+                    Object jsonObj = JSON.toJSON(args[i]);
+                    params.append(jsonObj.toString());
+                    if (i < args.length - 1) {
+                        params.append(", ");
+                    }
+                } catch (Exception e) {
+                    params.append(args[i].getClass().getSimpleName());
+                }
+            }
+        }
+
+        return params.length() > 0 ? params.toString() : "无参数";
+    }
+
+    private boolean isFilterObject(final Object o) {
+        Class<?> clazz = o.getClass();
+        if (clazz.isArray()) {
+            return clazz.getComponentType().isAssignableFrom(MultipartFile.class);
+        } else if (Collection.class.isAssignableFrom(clazz)) {
+            Collection collection = (Collection) o;
+            for (Iterator iter = collection.iterator(); iter.hasNext();) {
+                return iter.next() instanceof MultipartFile;
+            }
+        } else if (Map.class.isAssignableFrom(clazz)) {
+            Map map = (Map) o;
+            for (Iterator iter = map.entrySet().iterator(); iter.hasNext();) {
+                Map.Entry entry = (Map.Entry) iter.next();
+                return entry.getValue() instanceof MultipartFile;
+            }
+        }
+        return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse
+                || o instanceof BindingResult;
+    }
+}

+ 182 - 0
fs-ad-new-api/src/main/java/com/fs/framework/aspectj/DataScopeAspect.java

@@ -0,0 +1,182 @@
+package com.fs.framework.aspectj;
+
+import com.fs.common.annotation.DataScope;
+import com.fs.common.core.domain.BaseEntity;
+import com.fs.common.core.domain.entity.SysRole;
+import com.fs.common.core.domain.entity.SysUser;
+import com.fs.common.core.domain.model.LoginUser;
+import com.fs.common.utils.SecurityUtils;
+import com.fs.common.utils.StringUtils;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.Signature;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Before;
+import org.aspectj.lang.annotation.Pointcut;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.springframework.stereotype.Component;
+
+import java.lang.reflect.Method;
+
+/**
+ * 数据过滤处理
+ *
+
+ */
+@Aspect
+@Component
+public class DataScopeAspect
+{
+    /**
+     * 全部数据权限
+     */
+    public static final String DATA_SCOPE_ALL = "1";
+
+    /**
+     * 自定数据权限
+     */
+    public static final String DATA_SCOPE_CUSTOM = "2";
+
+    /**
+     * 部门数据权限
+     */
+    public static final String DATA_SCOPE_DEPT = "3";
+
+    /**
+     * 部门及以下数据权限
+     */
+    public static final String DATA_SCOPE_DEPT_AND_CHILD = "4";
+
+    /**
+     * 仅本人数据权限
+     */
+    public static final String DATA_SCOPE_SELF = "5";
+
+    /**
+     * 数据权限过滤关键字
+     */
+    public static final String DATA_SCOPE = "dataScope";
+
+    // 配置织入点
+    @Pointcut("@annotation(com.fs.common.annotation.DataScope)")
+    public void dataScopePointCut()
+    {
+    }
+
+    @Before("dataScopePointCut()")
+    public void doBefore(JoinPoint point) throws Throwable
+    {
+        clearDataScope(point);
+        handleDataScope(point);
+    }
+
+    protected void handleDataScope(final JoinPoint joinPoint)
+    {
+        // 获得注解
+        DataScope controllerDataScope = getAnnotationLog(joinPoint);
+        if (controllerDataScope == null)
+        {
+            return;
+        }
+        // 获取当前的用户
+        LoginUser loginUser = SecurityUtils.getLoginUser();
+        if (StringUtils.isNotNull(loginUser))
+        {
+            SysUser currentUser = loginUser.getUser();
+            // 如果是超级管理员,则不过滤数据
+            if (StringUtils.isNotNull(currentUser) && !currentUser.isAdmin())
+            {
+                dataScopeFilter(joinPoint, currentUser, controllerDataScope.deptAlias(),
+                        controllerDataScope.userAlias());
+            }
+        }
+    }
+
+    /**
+     * 数据范围过滤
+     *
+     * @param joinPoint 切点
+     * @param user 用户
+     * @param userAlias 别名
+     */
+    public static void dataScopeFilter(JoinPoint joinPoint, SysUser user, String deptAlias, String userAlias)
+    {
+        StringBuilder sqlString = new StringBuilder();
+
+        for (SysRole role : user.getRoles())
+        {
+            String dataScope = role.getDataScope();
+            if (DATA_SCOPE_ALL.equals(dataScope))
+            {
+                sqlString = new StringBuilder();
+                break;
+            }
+            else if (DATA_SCOPE_CUSTOM.equals(dataScope))
+            {
+                sqlString.append(StringUtils.format(
+                        " OR {}.dept_id IN ( SELECT dept_id FROM sys_role_dept WHERE role_id = {} ) ", deptAlias,
+                        role.getRoleId()));
+            }
+            else if (DATA_SCOPE_DEPT.equals(dataScope))
+            {
+                sqlString.append(StringUtils.format(" OR {}.dept_id = {} ", deptAlias, user.getDeptId()));
+            }
+            else if (DATA_SCOPE_DEPT_AND_CHILD.equals(dataScope))
+            {
+                sqlString.append(StringUtils.format(
+                        " OR {}.dept_id IN ( SELECT dept_id FROM sys_dept WHERE dept_id = {} or find_in_set( {} , ancestors ) )",
+                        deptAlias, user.getDeptId(), user.getDeptId()));
+            }
+            else if (DATA_SCOPE_SELF.equals(dataScope))
+            {
+                if (StringUtils.isNotBlank(userAlias))
+                {
+                    sqlString.append(StringUtils.format(" OR {}.user_id = {} ", userAlias, user.getUserId()));
+                }
+                else
+                {
+                    // 数据权限为仅本人且没有userAlias别名不查询任何数据
+                    sqlString.append(" OR 1=0 ");
+                }
+            }
+        }
+
+        if (StringUtils.isNotBlank(sqlString.toString()))
+        {
+            Object params = joinPoint.getArgs()[0];
+            if (StringUtils.isNotNull(params) && params instanceof BaseEntity)
+            {
+                BaseEntity baseEntity = (BaseEntity) params;
+                baseEntity.getParams().put(DATA_SCOPE, " AND (" + sqlString.substring(4) + ")");
+            }
+        }
+    }
+
+    /**
+     * 是否存在注解,如果存在就获取
+     */
+    private DataScope getAnnotationLog(JoinPoint joinPoint)
+    {
+        Signature signature = joinPoint.getSignature();
+        MethodSignature methodSignature = (MethodSignature) signature;
+        Method method = methodSignature.getMethod();
+
+        if (method != null)
+        {
+            return method.getAnnotation(DataScope.class);
+        }
+        return null;
+    }
+
+    /**
+     * 拼接权限sql前先清空params.dataScope参数防止注入
+     */
+    private void clearDataScope(final JoinPoint joinPoint)
+    {
+        Object params = joinPoint.getArgs()[0];
+        if (StringUtils.isNotNull(params) && params instanceof BaseEntity)
+        {
+            BaseEntity baseEntity = (BaseEntity) params;
+            baseEntity.getParams().put(DATA_SCOPE, "");
+        }
+    }
+}

+ 73 - 0
fs-ad-new-api/src/main/java/com/fs/framework/aspectj/DataSourceAspect.java

@@ -0,0 +1,73 @@
+package com.fs.framework.aspectj;
+
+import com.fs.common.annotation.DataSource;
+import com.fs.common.utils.StringUtils;
+import com.fs.framework.datasource.DynamicDataSourceContextHolder;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Pointcut;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.core.annotation.AnnotationUtils;
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Component;
+
+import java.util.Objects;
+
+/**
+ * 多数据源处理
+ * 
+
+ */
+@Aspect
+@Order(1)
+@Component
+public class DataSourceAspect
+{
+    protected Logger logger = LoggerFactory.getLogger(getClass());
+
+    @Pointcut("@annotation(com.fs.common.annotation.DataSource)"
+            + "|| @within(com.fs.common.annotation.DataSource)")
+    public void dsPointCut()
+    {
+
+    }
+
+    @Around("dsPointCut()")
+    public Object around(ProceedingJoinPoint point) throws Throwable
+    {
+        DataSource dataSource = getDataSource(point);
+
+        if (StringUtils.isNotNull(dataSource))
+        {
+            DynamicDataSourceContextHolder.setDataSourceType(dataSource.value().name());
+        }
+
+        try
+        {
+            return point.proceed();
+        }
+        finally
+        {
+            // 销毁数据源 在执行方法之后
+            DynamicDataSourceContextHolder.clearDataSourceType();
+        }
+    }
+
+    /**
+     * 获取需要切换的数据源
+     */
+    public DataSource getDataSource(ProceedingJoinPoint point)
+    {
+        MethodSignature signature = (MethodSignature) point.getSignature();
+        DataSource dataSource = AnnotationUtils.findAnnotation(signature.getMethod(), DataSource.class);
+        if (Objects.nonNull(dataSource))
+        {
+            return dataSource;
+        }
+
+        return AnnotationUtils.findAnnotation(signature.getDeclaringType(), DataSource.class);
+    }
+}

+ 117 - 0
fs-ad-new-api/src/main/java/com/fs/framework/aspectj/DistributeLockAspect.java

@@ -0,0 +1,117 @@
+package com.fs.framework.aspectj;
+
+import com.fs.common.annotation.DistributeLock;
+import com.fs.common.constant.DistributeLockConstant;
+import com.fs.common.exception.DistributeLockException;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.redisson.api.RLock;
+import org.redisson.api.RedissonClient;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.core.StandardReflectionParameterNameDiscoverer;
+import org.springframework.core.annotation.Order;
+import org.springframework.expression.EvaluationContext;
+import org.springframework.expression.Expression;
+import org.springframework.expression.spel.standard.SpelExpressionParser;
+import org.springframework.expression.spel.support.StandardEvaluationContext;
+import org.springframework.stereotype.Component;
+
+import java.lang.reflect.Method;
+import java.util.concurrent.TimeUnit;
+
+@Aspect
+@Component
+@Order(Integer.MIN_VALUE + 1)
+public class DistributeLockAspect {
+
+    private RedissonClient redissonClient;
+
+    public DistributeLockAspect(RedissonClient redissonClient) {
+        this.redissonClient = redissonClient;
+    }
+
+    private static final Logger LOG = LoggerFactory.getLogger(DistributeLockAspect.class);
+
+    @Around("@annotation(com.fs.common.annotation.DistributeLock)")
+    public Object process(ProceedingJoinPoint pjp) throws Throwable {
+        Object response = null;
+        Method method = ((MethodSignature) pjp.getSignature()).getMethod();
+        DistributeLock distributeLock = method.getAnnotation(DistributeLock.class);
+
+        String key = distributeLock.key();
+        if (DistributeLockConstant.NONE_KEY.equals(key)) {
+            if (DistributeLockConstant.NONE_KEY.equals(distributeLock.keyExpression())) {
+                throw new DistributeLockException("no lock key found...");
+            }
+            SpelExpressionParser parser = new SpelExpressionParser();
+            Expression expression = parser.parseExpression(distributeLock.keyExpression());
+
+            EvaluationContext context = new StandardEvaluationContext();
+            // 获取参数值
+            Object[] args = pjp.getArgs();
+
+            // 获取运行时参数的名称
+            StandardReflectionParameterNameDiscoverer discoverer
+                    = new StandardReflectionParameterNameDiscoverer();
+            String[] parameterNames = discoverer.getParameterNames(method);
+
+            // 将参数绑定到context中
+            if (parameterNames != null) {
+                for (int i = 0; i < parameterNames.length; i++) {
+                    context.setVariable(parameterNames[i], args[i]);
+                }
+            }
+
+            // 解析表达式,获取结果
+            key = String.valueOf(expression.getValue(context));
+        }
+
+        String scene = distributeLock.scene();
+
+        String lockKey = scene + "#" + key;
+
+        int expireTime = distributeLock.expireTime();
+        int waitTime = distributeLock.waitTime();
+        RLock rLock= redissonClient.getLock(lockKey);
+        try {
+            boolean lockResult = false;
+            if (waitTime == DistributeLockConstant.DEFAULT_WAIT_TIME) {
+                if (expireTime == DistributeLockConstant.DEFAULT_EXPIRE_TIME) {
+                    LOG.info(String.format("lock for key : %s", lockKey));
+                    rLock.lock();
+                } else {
+                    LOG.info(String.format("lock for key : %s , expire : %s", lockKey, expireTime));
+                    rLock.lock(expireTime, TimeUnit.MILLISECONDS);
+                }
+                lockResult = true;
+            } else {
+                if (expireTime == DistributeLockConstant.DEFAULT_EXPIRE_TIME) {
+                    LOG.info(String.format("try lock for key : %s , wait : %s", lockKey, waitTime));
+                    lockResult = rLock.tryLock(waitTime, TimeUnit.MILLISECONDS);
+                } else {
+                    LOG.info(String.format("try lock for key : %s , expire : %s , wait : %s", lockKey, expireTime, waitTime));
+                    lockResult = rLock.tryLock(waitTime, expireTime, TimeUnit.MILLISECONDS);
+                }
+            }
+
+            if (!lockResult) {
+                LOG.warn(String.format("lock failed for key : %s , expire : %s", lockKey, expireTime));
+                throw new DistributeLockException(distributeLock.errorMsg());
+            }
+
+
+            LOG.info(String.format("lock success for key : %s , expire : %s", lockKey, expireTime));
+            response = pjp.proceed();
+        }  finally {
+            if (rLock.isHeldByCurrentThread()) {
+                rLock.unlock();
+                LOG.info(String.format("unlock for key : %s , expire : %s", lockKey, expireTime));
+            }
+        }
+        return response;
+    }
+}

+ 244 - 0
fs-ad-new-api/src/main/java/com/fs/framework/aspectj/LogAspect.java

@@ -0,0 +1,244 @@
+package com.fs.framework.aspectj;
+
+import com.alibaba.fastjson.JSON;
+import com.fs.common.annotation.Log;
+import com.fs.common.core.domain.model.LoginUser;
+import com.fs.common.enums.BusinessStatus;
+import com.fs.common.enums.HttpMethod;
+import com.fs.common.utils.SecurityUtils;
+import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.StringUtils;
+import com.fs.common.utils.ip.IpUtils;
+import com.fs.framework.manager.AsyncManager;
+import com.fs.framework.manager.factory.AsyncFactory;
+import com.fs.system.domain.SysOperLog;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.Signature;
+import org.aspectj.lang.annotation.AfterReturning;
+import org.aspectj.lang.annotation.AfterThrowing;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Pointcut;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+import org.springframework.validation.BindingResult;
+import org.springframework.web.multipart.MultipartFile;
+import org.springframework.web.servlet.HandlerMapping;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.lang.reflect.Method;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.Map;
+
+/**
+ * 操作日志记录处理
+ *
+
+ */
+@Aspect
+@Component
+public class LogAspect
+{
+    private static final Logger log = LoggerFactory.getLogger(LogAspect.class);
+
+    // 配置织入点
+    @Pointcut("@annotation(com.fs.common.annotation.Log)")
+    public void logPointCut()
+    {
+    }
+
+    /**
+     * 处理完请求后执行
+     *
+     * @param joinPoint 切点
+     */
+    @AfterReturning(pointcut = "logPointCut()", returning = "jsonResult")
+    public void doAfterReturning(JoinPoint joinPoint, Object jsonResult)
+    {
+        handleLog(joinPoint, null, jsonResult);
+    }
+
+    /**
+     * 拦截异常操作
+     *
+     * @param joinPoint 切点
+     * @param e 异常
+     */
+    @AfterThrowing(value = "logPointCut()", throwing = "e")
+    public void doAfterThrowing(JoinPoint joinPoint, Exception e)
+    {
+        handleLog(joinPoint, e, null);
+    }
+
+    protected void handleLog(final JoinPoint joinPoint, final Exception e, Object jsonResult)
+    {
+        try
+        {
+            // 获得注解
+            Log controllerLog = getAnnotationLog(joinPoint);
+            if (controllerLog == null)
+            {
+                return;
+            }
+
+            // 获取当前的用户
+            LoginUser loginUser = SecurityUtils.getLoginUser();
+
+            // *========数据库日志=========*//
+            SysOperLog operLog = new SysOperLog();
+            operLog.setStatus(BusinessStatus.SUCCESS.ordinal());
+            // 请求的地址
+            String ip = IpUtils.getIpAddr(ServletUtils.getRequest());
+            operLog.setOperIp(ip);
+            // 返回参数
+            operLog.setJsonResult(JSON.toJSONString(jsonResult));
+
+            operLog.setOperUrl(ServletUtils.getRequest().getRequestURI());
+            if (loginUser != null)
+            {
+                operLog.setOperName(loginUser.getUsername());
+            }
+
+            if (e != null)
+            {
+                operLog.setStatus(BusinessStatus.FAIL.ordinal());
+                operLog.setErrorMsg(StringUtils.substring(e.getMessage(), 0, 2000));
+            }
+            // 设置方法名称
+            String className = joinPoint.getTarget().getClass().getName();
+            String methodName = joinPoint.getSignature().getName();
+            operLog.setMethod(className + "." + methodName + "()");
+            // 设置请求方式
+            operLog.setRequestMethod(ServletUtils.getRequest().getMethod());
+            // 处理设置注解上的参数
+            getControllerMethodDescription(joinPoint, controllerLog, operLog);
+            // 保存数据库
+            AsyncManager.me().execute(AsyncFactory.recordOper(operLog));
+        }
+        catch (Exception exp)
+        {
+            // 记录本地异常日志
+            log.error("==前置通知异常==");
+            log.error("异常信息:{}", exp.getMessage());
+            exp.printStackTrace();
+        }
+    }
+
+    /**
+     * 获取注解中对方法的描述信息 用于Controller层注解
+     *
+     * @param log 日志
+     * @param operLog 操作日志
+     * @throws Exception
+     */
+    public void getControllerMethodDescription(JoinPoint joinPoint, Log log, SysOperLog operLog) throws Exception
+    {
+        // 设置action动作
+        operLog.setBusinessType(log.businessType().ordinal());
+        // 设置标题
+        operLog.setTitle(log.title());
+        // 设置操作人类别
+        operLog.setOperatorType(log.operatorType().ordinal());
+        // 是否需要保存request,参数和值
+        if (log.isSaveRequestData())
+        {
+            // 获取参数的信息,传入到数据库中。
+            setRequestValue(joinPoint, operLog);
+        }
+    }
+
+    /**
+     * 获取请求的参数,放到log中
+     *
+     * @param operLog 操作日志
+     * @throws Exception 异常
+     */
+    private void setRequestValue(JoinPoint joinPoint, SysOperLog operLog) throws Exception
+    {
+        String requestMethod = operLog.getRequestMethod();
+        if (HttpMethod.PUT.name().equals(requestMethod) || HttpMethod.POST.name().equals(requestMethod))
+        {
+            String params = argsArrayToString(joinPoint.getArgs());
+            operLog.setOperParam(StringUtils.substring(params, 0, 2000));
+        }
+        else
+        {
+            Map<?, ?> paramsMap = (Map<?, ?>) ServletUtils.getRequest().getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
+            operLog.setOperParam(StringUtils.substring(paramsMap.toString(), 0, 2000));
+        }
+    }
+
+    /**
+     * 是否存在注解,如果存在就获取
+     */
+    private Log getAnnotationLog(JoinPoint joinPoint) throws Exception
+    {
+        Signature signature = joinPoint.getSignature();
+        MethodSignature methodSignature = (MethodSignature) signature;
+        Method method = methodSignature.getMethod();
+
+        if (method != null)
+        {
+            return method.getAnnotation(Log.class);
+        }
+        return null;
+    }
+
+    /**
+     * 参数拼装
+     */
+    private String argsArrayToString(Object[] paramsArray)
+    {
+        String params = "";
+        if (paramsArray != null && paramsArray.length > 0)
+        {
+            for (int i = 0; i < paramsArray.length; i++)
+            {
+                if (StringUtils.isNotNull(paramsArray[i]) && !isFilterObject(paramsArray[i]))
+                {
+                    Object jsonObj = JSON.toJSON(paramsArray[i]);
+                    params += jsonObj.toString() + " ";
+                }
+            }
+        }
+        return params.trim();
+    }
+
+    /**
+     * 判断是否需要过滤的对象。
+     *
+     * @param o 对象信息。
+     * @return 如果是需要过滤的对象,则返回true;否则返回false。
+     */
+    @SuppressWarnings("rawtypes")
+    public boolean isFilterObject(final Object o)
+    {
+        Class<?> clazz = o.getClass();
+        if (clazz.isArray())
+        {
+            return clazz.getComponentType().isAssignableFrom(MultipartFile.class);
+        }
+        else if (Collection.class.isAssignableFrom(clazz))
+        {
+            Collection collection = (Collection) o;
+            for (Iterator iter = collection.iterator(); iter.hasNext();)
+            {
+                return iter.next() instanceof MultipartFile;
+            }
+        }
+        else if (Map.class.isAssignableFrom(clazz))
+        {
+            Map map = (Map) o;
+            for (Iterator iter = map.entrySet().iterator(); iter.hasNext();)
+            {
+                Map.Entry entry = (Map.Entry) iter.next();
+                return entry.getValue() instanceof MultipartFile;
+            }
+        }
+        return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse
+                || o instanceof BindingResult;
+    }
+}

+ 58 - 0
fs-ad-new-api/src/main/java/com/fs/framework/aspectj/ScheduledTaskAspect.java

@@ -0,0 +1,58 @@
+package com.fs.framework.aspectj;
+
+import lombok.extern.slf4j.Slf4j;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Pointcut;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StopWatch;
+
+/**
+ * 定时任务执行时间切面
+ * 记录所有定时任务的执行时间和状态
+ *
+ * @author zhangqin
+ * @date 2025-12-15
+ */
+@Slf4j
+@Aspect
+@Component
+public class ScheduledTaskAspect {
+
+    @Pointcut("@annotation(org.springframework.scheduling.annotation.Scheduled) && execution(* com.fs.app.task..*(..))")
+    public void scheduledTaskPointcut() {
+    }
+
+    @Around("scheduledTaskPointcut()")
+    public Object aroundScheduledTask(ProceedingJoinPoint joinPoint) throws Throwable {
+        String taskName = getTaskName(joinPoint);
+        StopWatch stopWatch = new StopWatch(taskName);
+        stopWatch.start();
+
+        try {
+            Object result = joinPoint.proceed();
+            stopWatch.stop();
+            long executionTime = stopWatch.getTotalTimeMillis();
+            log.info("任务执行耗时: {} {}", taskName, executionTime);
+            return result;
+        } catch (Exception e) {
+            stopWatch.stop();
+            long executionTime = stopWatch.getTotalTimeMillis();
+            log.error("==================== 定时任务执行失败 ====================");
+            log.error("任务名称: [{}]", taskName);
+            log.error("执行耗时: [{}ms]", executionTime);
+            log.error("执行状态: [失败]");
+            log.error("错误信息: [{}]", e.getMessage(), e);
+            throw e;
+        }
+    }
+
+    /**
+     * 获取任务名称
+     */
+    private String getTaskName(ProceedingJoinPoint joinPoint) {
+        return joinPoint.getSignature().getName();
+    }
+}
+

+ 29 - 0
fs-ad-new-api/src/main/java/com/fs/framework/aspectj/ScheduledTraceIdAspect.java

@@ -0,0 +1,29 @@
+package com.fs.framework.aspectj;
+
+import com.fs.common.utils.TraceIdUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Pointcut;
+import org.springframework.stereotype.Component;
+
+@Aspect
+@Component
+@Slf4j
+public class ScheduledTraceIdAspect {
+
+    @Pointcut("@annotation(org.springframework.scheduling.annotation.Scheduled)")
+    public void scheduledPointcut() {
+    }
+
+    @Around("scheduledPointcut()")
+    public Object around(ProceedingJoinPoint pjp) throws Throwable {
+        try {
+            TraceIdUtil.init();
+            return pjp.proceed();
+        } finally {
+            TraceIdUtil.clear();
+        }
+    }
+}

+ 0 - 0
fs-hospital/src/main/java/com/fs/framework/config/ApplicationConfig.java → fs-ad-new-api/src/main/java/com/fs/framework/config/ApplicationConfig.java


+ 61 - 0
fs-ad-new-api/src/main/java/com/fs/framework/config/AsyncConfig.java

@@ -0,0 +1,61 @@
+package com.fs.framework.config;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.annotation.AsyncConfigurer;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.ThreadPoolExecutor;
+
+/**
+ * 异步任务配置类
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ */
+@Slf4j
+@Configuration
+public class AsyncConfig implements AsyncConfigurer {
+
+    @Bean(name = "asyncExecutor")
+    @Override
+    public Executor getAsyncExecutor() {
+        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
+
+        // 核心线程数
+        executor.setCorePoolSize(10);
+
+        // 最大线程数
+        executor.setMaxPoolSize(50);
+
+        // 队列容量
+        executor.setQueueCapacity(1000);
+
+        // 线程存活时间(秒)
+        executor.setKeepAliveSeconds(60);
+
+        // 线程名称前缀
+        executor.setThreadNamePrefix("async-task-");
+
+        // 拒绝策略:调用者运行
+        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
+
+        // 等待所有任务完成后再关闭线程池
+        executor.setWaitForTasksToCompleteOnShutdown(true);
+
+        // 等待时间
+        executor.setAwaitTerminationSeconds(60);
+
+
+        // ⭐ 关键:传递 MDC
+        executor.setTaskDecorator(new MdcTaskDecorator());
+
+        executor.initialize();
+
+        log.info("异步线程池初始化完成");
+        return executor;
+    }
+}
+

+ 19 - 0
fs-ad-new-api/src/main/java/com/fs/framework/config/CorsConfig.java

@@ -0,0 +1,19 @@
+package com.fs.framework.config;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.CorsRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+@Configuration
+public class CorsConfig implements WebMvcConfigurer {
+
+    @Override
+    public void addCorsMappings(CorsRegistry registry) {
+        registry.addMapping("/**")
+                .allowedOrigins("*") // ★旧版本用这个
+                .allowedMethods("*")
+                .allowedHeaders("*")
+                .allowCredentials(false) // ★注意:allowedOrigins("*") 不能和 true 一起用
+                .maxAge(3600);
+    }
+}

+ 92 - 0
fs-ad-new-api/src/main/java/com/fs/framework/config/DataSourceConfig.java

@@ -0,0 +1,92 @@
+package com.fs.framework.config;
+
+import com.alibaba.druid.pool.DruidDataSource;
+import com.alibaba.druid.spring.boot.autoconfigure.properties.DruidStatProperties;
+import com.alibaba.druid.util.Utils;
+import com.fs.common.enums.DataSourceType;
+import com.fs.framework.datasource.DynamicDataSource;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.web.servlet.FilterRegistrationBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Primary;
+
+import javax.servlet.*;
+import javax.sql.DataSource;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+@Configuration
+public class DataSourceConfig {
+
+    @Bean
+    @ConfigurationProperties(prefix = "spring.datasource.sop.druid.master")
+    public DataSource sopDataSource() {
+        return new DruidDataSource();
+    }
+
+    @Bean
+    @ConfigurationProperties(prefix = "spring.datasource.mysql.druid.master")
+    public DataSource masterDataSource() {
+        return new DruidDataSource();
+    }
+
+
+
+    @Bean
+    @Primary
+    public DynamicDataSource dataSource(@Qualifier("masterDataSource") DataSource masterDataSource, @Qualifier("sopDataSource") DataSource sopDataSource) {
+        Map<Object, Object> targetDataSources = new HashMap<>();
+        targetDataSources.put(DataSourceType.SOP.name(), sopDataSource);
+        return new DynamicDataSource(masterDataSource, targetDataSources);
+    }
+
+    /**
+     * 去除监控页面底部的广告
+     */
+    @SuppressWarnings({ "rawtypes", "unchecked" })
+    @Bean
+    @ConditionalOnProperty(name = "spring.datasource.mysql.druid.statViewServlet.enabled", havingValue = "true")
+    public FilterRegistrationBean removeDruidFilterRegistrationBean(DruidStatProperties properties)
+    {
+        // 获取web监控页面的参数
+        DruidStatProperties.StatViewServlet config = properties.getStatViewServlet();
+        // 提取common.js的配置路径
+        String pattern = config.getUrlPattern() != null ? config.getUrlPattern() : "/druid/*";
+        String commonJsPattern = pattern.replaceAll("\\*", "js/common.js");
+        final String filePath = "support/http/resources/js/common.js";
+        // 创建filter进行过滤
+        Filter filter = new Filter()
+        {
+            @Override
+            public void init(javax.servlet.FilterConfig filterConfig) throws ServletException
+            {
+            }
+            @Override
+            public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
+                    throws IOException, ServletException
+            {
+                chain.doFilter(request, response);
+                // 重置缓冲区,响应头不会被重置
+                response.resetBuffer();
+                // 获取common.js
+                String text = Utils.readFromResource(filePath);
+                // 正则替换banner, 除去底部的广告信息
+                text = text.replaceAll("<a.*?banner\"></a><br/>", "");
+                text = text.replaceAll("powered.*?shrek.wang</a>", "");
+                response.getWriter().write(text);
+            }
+            @Override
+            public void destroy()
+            {
+            }
+        };
+        FilterRegistrationBean registrationBean = new FilterRegistrationBean();
+        registrationBean.setFilter(filter);
+        registrationBean.addUrlPatterns(commonJsPattern);
+        return registrationBean;
+    }
+}

+ 72 - 0
fs-ad-new-api/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java

@@ -0,0 +1,72 @@
+package com.fs.framework.config;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.parser.ParserConfig;
+import com.alibaba.fastjson.serializer.SerializerFeature;
+import com.fasterxml.jackson.databind.JavaType;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.type.TypeFactory;
+import org.springframework.data.redis.serializer.RedisSerializer;
+import org.springframework.data.redis.serializer.SerializationException;
+import org.springframework.util.Assert;
+
+import java.nio.charset.Charset;
+
+/**
+ * Redis使用FastJson序列化
+ * 
+
+ */
+public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
+{
+    @SuppressWarnings("unused")
+    private ObjectMapper objectMapper = new ObjectMapper();
+
+    public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
+
+    private Class<T> clazz;
+
+    static
+    {
+        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
+    }
+
+    public FastJson2JsonRedisSerializer(Class<T> clazz)
+    {
+        super();
+        this.clazz = clazz;
+    }
+
+    @Override
+    public byte[] serialize(T t) throws SerializationException
+    {
+        if (t == null)
+        {
+            return new byte[0];
+        }
+        return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
+    }
+
+    @Override
+    public T deserialize(byte[] bytes) throws SerializationException
+    {
+        if (bytes == null || bytes.length <= 0)
+        {
+            return null;
+        }
+        String str = new String(bytes, DEFAULT_CHARSET);
+
+        return JSON.parseObject(str, clazz);
+    }
+
+    public void setObjectMapper(ObjectMapper objectMapper)
+    {
+        Assert.notNull(objectMapper, "'objectMapper' must not be null");
+        this.objectMapper = objectMapper;
+    }
+
+    protected JavaType getJavaType(Class<?> clazz)
+    {
+        return TypeFactory.defaultInstance().constructType(clazz);
+    }
+}

+ 0 - 0
fs-hospital/src/main/java/com/fs/framework/config/FilterConfig.java → fs-ad-new-api/src/main/java/com/fs/framework/config/FilterConfig.java


+ 30 - 0
fs-ad-new-api/src/main/java/com/fs/framework/config/MdcTaskDecorator.java

@@ -0,0 +1,30 @@
+package com.fs.framework.config;
+
+import org.slf4j.MDC;
+import org.springframework.core.task.TaskDecorator;
+
+import java.util.Map;
+
+public class MdcTaskDecorator implements TaskDecorator {
+
+    @Override
+    public Runnable decorate(Runnable runnable) {
+
+        // 1. 获取提交任务时的 MDC 内容
+        Map<String, String> contextMap = MDC.getCopyOfContextMap();
+
+        return () -> {
+            try {
+                // 2. 任务执行前恢复 MDC
+                if (contextMap != null) {
+                    MDC.setContextMap(contextMap);
+                }
+
+                runnable.run();
+            } finally {
+                // 3. 清理,避免线程复用污染
+                MDC.clear();
+            }
+        };
+    }
+}

+ 148 - 0
fs-ad-new-api/src/main/java/com/fs/framework/config/MyBatisConfig.java

@@ -0,0 +1,148 @@
+package com.fs.framework.config;
+
+import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
+import org.apache.ibatis.io.VFS;
+import org.apache.ibatis.session.SqlSessionFactory;
+import org.mybatis.spring.boot.autoconfigure.SpringBootVFS;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.env.Environment;
+import org.springframework.core.io.DefaultResourceLoader;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
+import org.springframework.core.io.support.ResourcePatternResolver;
+import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
+import org.springframework.core.type.classreading.MetadataReader;
+import org.springframework.core.type.classreading.MetadataReaderFactory;
+import org.springframework.util.ClassUtils;
+
+import javax.sql.DataSource;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+
+/**
+ * Mybatis支持*匹配扫描包
+ *
+
+ */
+@Configuration
+public class MyBatisConfig
+{
+    @Autowired
+    private Environment env;
+
+    static final String DEFAULT_RESOURCE_PATTERN = "**/*.class";
+
+    public static String setTypeAliasesPackage(String typeAliasesPackage)
+    {
+        ResourcePatternResolver resolver = (ResourcePatternResolver) new PathMatchingResourcePatternResolver();
+        MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(resolver);
+        List<String> allResult = new ArrayList<String>();
+        try
+        {
+            for (String aliasesPackage : typeAliasesPackage.split(","))
+            {
+                List<String> result = new ArrayList<String>();
+                aliasesPackage = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX
+                        + ClassUtils.convertClassNameToResourcePath(aliasesPackage.trim()) + "/" + DEFAULT_RESOURCE_PATTERN;
+                Resource[] resources = resolver.getResources(aliasesPackage);
+                if (resources != null && resources.length > 0)
+                {
+                    MetadataReader metadataReader = null;
+                    for (Resource resource : resources)
+                    {
+                        if (resource.isReadable())
+                        {
+                            metadataReader = metadataReaderFactory.getMetadataReader(resource);
+                            try
+                            {
+                                result.add(Class.forName(metadataReader.getClassMetadata().getClassName()).getPackage().getName());
+                            }
+                            catch (ClassNotFoundException e)
+                            {
+                                e.printStackTrace();
+                            }
+                        }
+                    }
+                }
+                if (result.size() > 0)
+                {
+                    HashSet<String> hashResult = new HashSet<String>(result);
+                    allResult.addAll(hashResult);
+                }
+            }
+            if (allResult.size() > 0)
+            {
+                typeAliasesPackage = String.join(",", (String[]) allResult.toArray(new String[0]));
+            }
+            else
+            {
+                throw new RuntimeException("mybatis typeAliasesPackage 路径扫描错误,参数typeAliasesPackage:" + typeAliasesPackage + "未找到任何包");
+            }
+        }
+        catch (IOException e)
+        {
+            e.printStackTrace();
+        }
+        return typeAliasesPackage;
+    }
+
+    public Resource[] resolveMapperLocations(String[] mapperLocations)
+    {
+        ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();
+        List<Resource> resources = new ArrayList<Resource>();
+        if (mapperLocations != null)
+        {
+            for (String mapperLocation : mapperLocations)
+            {
+                try
+                {
+                    Resource[] mappers = resourceResolver.getResources(mapperLocation);
+                    resources.addAll(Arrays.asList(mappers));
+                }
+                catch (IOException e)
+                {
+                    // ignore
+                }
+            }
+        }
+        return resources.toArray(new Resource[resources.size()]);
+    }
+
+//    @Bean
+//    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception
+//    {
+//        String typeAliasesPackage = env.getProperty("mybatis.typeAliasesPackage");
+//        String mapperLocations = env.getProperty("mybatis.mapperLocations");
+//        String configLocation = env.getProperty("mybatis.configLocation");
+//        typeAliasesPackage = setTypeAliasesPackage(typeAliasesPackage);
+//        VFS.addImplClass(SpringBootVFS.class);
+//
+//        final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
+//        sessionFactory.setDataSource(dataSource);
+//        sessionFactory.setTypeAliasesPackage(typeAliasesPackage);
+//        sessionFactory.setMapperLocations(resolveMapperLocations(StringUtils.split(mapperLocations, ",")));
+//        sessionFactory.setConfigLocation(new DefaultResourceLoader().getResource(configLocation));
+//        return sessionFactory.getObject();
+//    }
+    @Bean
+    public SqlSessionFactory sqlSessionFactorys(DataSource dataSource) throws Exception
+    {
+        String typeAliasesPackage = env.getProperty("mybatis-plus.typeAliasesPackage");
+        String mapperLocations = env.getProperty("mybatis-plus.mapperLocations");
+        String configLocation = env.getProperty("mybatis-plus.configLocation");
+        typeAliasesPackage = setTypeAliasesPackage(typeAliasesPackage);
+        VFS.addImplClass(SpringBootVFS.class);
+
+        final MybatisSqlSessionFactoryBean sessionFactory = new MybatisSqlSessionFactoryBean();
+        sessionFactory.setDataSource(dataSource);
+        sessionFactory.setTypeAliasesPackage(typeAliasesPackage);
+        sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
+        sessionFactory.setConfigLocation(new DefaultResourceLoader().getResource(configLocation));
+        return sessionFactory.getObject();
+    }
+}

+ 158 - 0
fs-ad-new-api/src/main/java/com/fs/framework/config/RedisConfig.java

@@ -0,0 +1,158 @@
+package com.fs.framework.config;
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+import com.fasterxml.jackson.annotation.PropertyAccessor;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
+import org.springframework.cache.annotation.CachingConfigurerSupport;
+import org.springframework.cache.annotation.EnableCaching;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.script.DefaultRedisScript;
+import org.springframework.data.redis.serializer.GenericToStringSerializer;
+import org.springframework.data.redis.serializer.StringRedisSerializer;
+
+import java.math.BigDecimal;
+
+/**
+ * redis配置
+ *
+
+ */
+@Configuration
+@EnableCaching
+public class RedisConfig extends CachingConfigurerSupport
+{
+    @Bean
+    @SuppressWarnings(value = { "unchecked", "rawtypes" })
+    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory)
+    {
+        RedisTemplate<Object, Object> template = new RedisTemplate<>();
+        template.setConnectionFactory(connectionFactory);
+
+        FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
+
+        ObjectMapper mapper = new ObjectMapper();
+        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
+        mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
+        serializer.setObjectMapper(mapper);
+
+        // 使用StringRedisSerializer来序列化和反序列化redis的key值
+        template.setKeySerializer(new StringRedisSerializer());
+        template.setValueSerializer(serializer);
+
+        // Hash的key也采用StringRedisSerializer的序列化方式
+        template.setHashKeySerializer(new StringRedisSerializer());
+        template.setHashValueSerializer(serializer);
+
+        template.afterPropertiesSet();
+        return template;
+    }
+    @Bean
+    public RedisTemplate<String, Boolean> redisTemplateForBoolean(RedisConnectionFactory connectionFactory) {
+        RedisTemplate<String, Boolean> template = new RedisTemplate<>();
+        template.setConnectionFactory(connectionFactory);
+
+        // 使用StringRedisSerializer来序列化和反序列化redis的key值
+        template.setKeySerializer(new StringRedisSerializer());
+        template.setValueSerializer(new GenericToStringSerializer<>(Boolean.class));
+
+        // Hash的key也采用StringRedisSerializer的序列化方式
+        template.setHashKeySerializer(new StringRedisSerializer());
+        template.setHashValueSerializer(new GenericToStringSerializer<>(Boolean.class));
+
+        template.afterPropertiesSet();
+        return template;
+    }
+    @Bean
+    public RedisTemplate<String, Integer> redisTemplateForInteger(RedisConnectionFactory connectionFactory) {
+        RedisTemplate<String, Integer> template = new RedisTemplate<>();
+        template.setConnectionFactory(connectionFactory);
+
+        // 使用StringRedisSerializer来序列化和反序列化redis的key值
+        template.setKeySerializer(new StringRedisSerializer());
+
+        // 使用GenericToStringSerializer保证BigDecimal精度不丢失
+        template.setValueSerializer(new GenericToStringSerializer<>(Integer.class));
+
+        // Hash的key也采用StringRedisSerializer的序列化方式
+        template.setHashKeySerializer(new StringRedisSerializer());
+        template.setHashValueSerializer(new GenericToStringSerializer<>(Integer.class));
+
+        template.afterPropertiesSet();
+        return template;
+    }
+
+    @Bean
+    @SuppressWarnings(value = { "unchecked", "rawtypes" })
+    public RedisTemplate<String, Object> redisTemplateForObject(RedisConnectionFactory connectionFactory) {
+        RedisTemplate<String, Object> template = new RedisTemplate<>();
+        template.setConnectionFactory(connectionFactory);
+
+        FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
+
+        ObjectMapper mapper = new ObjectMapper();
+        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
+        mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
+        serializer.setObjectMapper(mapper);
+
+        // 使用StringRedisSerializer来序列化和反序列化redis的key值
+        template.setKeySerializer(new StringRedisSerializer());
+        template.setValueSerializer(serializer);
+
+        // Hash的key也采用StringRedisSerializer的序列化方式
+        template.setHashKeySerializer(new StringRedisSerializer());
+        template.setHashValueSerializer(serializer);
+
+        template.afterPropertiesSet();
+        return template;
+    }
+    @Bean
+    public RedisTemplate<String, BigDecimal> redisTemplateForBigDecimal(RedisConnectionFactory connectionFactory) {
+        RedisTemplate<String, BigDecimal> template = new RedisTemplate<>();
+        template.setConnectionFactory(connectionFactory);
+
+        // 使用StringRedisSerializer来序列化和反序列化redis的key值
+        template.setKeySerializer(new StringRedisSerializer());
+
+        // 使用GenericToStringSerializer保证BigDecimal精度不丢失
+        template.setValueSerializer(new GenericToStringSerializer<>(BigDecimal.class));
+
+        // Hash的key也采用StringRedisSerializer的序列化方式
+        template.setHashKeySerializer(new StringRedisSerializer());
+        template.setHashValueSerializer(new GenericToStringSerializer<>(BigDecimal.class));
+
+        template.afterPropertiesSet();
+        return template;
+    }
+    @Bean
+    public DefaultRedisScript<Long> limitScript()
+    {
+        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
+        redisScript.setScriptText(limitScriptText());
+        redisScript.setResultType(Long.class);
+        return redisScript;
+    }
+
+    /**
+     * 限流脚本
+     */
+    private String limitScriptText()
+    {
+        return "local key = KEYS[1]\n" +
+                "local count = tonumber(ARGV[1])\n" +
+                "local time = tonumber(ARGV[2])\n" +
+                "local current = redis.call('get', key);\n" +
+                "if current and tonumber(current) > count then\n" +
+                "    return current;\n" +
+                "end\n" +
+                "current = redis.call('incr', key)\n" +
+                "if tonumber(current) == 1 then\n" +
+                "    redis.call('expire', key, time)\n" +
+                "end\n" +
+                "return current;";
+    }
+}

+ 50 - 0
fs-ad-new-api/src/main/java/com/fs/framework/config/SecurityConfig.java

@@ -0,0 +1,50 @@
+package com.fs.framework.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.config.BeanIds;
+import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
+
+/**
+ * spring security配置
+ * 
+
+ */
+@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
+public class SecurityConfig extends WebSecurityConfigurerAdapter
+{
+
+    /**
+     * anyRequest          |   匹配所有请求路径
+     * access              |   SpringEl表达式结果为true时可以访问
+     * anonymous           |   匿名可以访问
+     * denyAll             |   用户不能访问
+     * fullyAuthenticated  |   用户完全认证可以访问(非remember-me下自动登录)
+     * hasAnyAuthority     |   如果有参数,参数表示权限,则其中任何一个权限可以访问
+     * hasAnyRole          |   如果有参数,参数表示角色,则其中任何一个角色可以访问
+     * hasAuthority        |   如果有参数,参数表示权限,则其权限可以访问
+     * hasIpAddress        |   如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
+     * hasRole             |   如果有参数,参数表示角色,则其角色可以访问
+     * permitAll           |   用户可以任意访问
+     * rememberMe          |   允许通过remember-me登录的用户访问
+     * authenticated       |   用户登录后可访问
+     */
+    @Override
+    protected void configure(HttpSecurity http) throws Exception
+    {
+        http.authorizeRequests()
+                .antMatchers("/**").permitAll()
+                .anyRequest().authenticated()
+                .and().csrf().disable();
+    }
+
+    @Bean(name = BeanIds.AUTHENTICATION_MANAGER)
+    @Override
+    public AuthenticationManager authenticationManagerBean() throws Exception {
+        return super.authenticationManagerBean();
+    }
+
+
+}

+ 33 - 0
fs-ad-new-api/src/main/java/com/fs/framework/config/ServerConfig.java

@@ -0,0 +1,33 @@
+package com.fs.framework.config;
+
+import com.fs.common.utils.ServletUtils;
+import org.springframework.stereotype.Component;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * 服务相关配置
+ * 
+
+ */
+@Component
+public class ServerConfig
+{
+    /**
+     * 获取完整的请求路径,包括:域名,端口,上下文访问路径
+     * 
+     * @return 服务地址
+     */
+    public String getUrl()
+    {
+        HttpServletRequest request = ServletUtils.getRequest();
+        return getDomain(request);
+    }
+
+    public static String getDomain(HttpServletRequest request)
+    {
+        StringBuffer url = request.getRequestURL();
+        String contextPath = request.getServletContext().getContextPath();
+        return url.delete(url.length() - request.getRequestURI().length(), url.length()).append(contextPath).toString();
+    }
+}

+ 0 - 0
fs-hospital/src/main/java/com/fs/framework/config/ThreadPoolConfig.java → fs-ad-new-api/src/main/java/com/fs/framework/config/ThreadPoolConfig.java


+ 36 - 0
fs-ad-new-api/src/main/java/com/fs/framework/config/TrackIdFilter.java

@@ -0,0 +1,36 @@
+package com.fs.framework.config;
+
+import cn.hutool.core.lang.UUID;
+import org.jboss.logging.MDC;
+import org.springframework.stereotype.Component;
+
+import javax.servlet.*;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+@Component
+public class TrackIdFilter implements Filter {
+
+    public static final String TRACK_ID = "X-Track-Id";
+
+    @Override
+    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
+            throws IOException, ServletException {
+
+        // 1. 生成唯一 trackId
+        String trackId = UUID.randomUUID().toString().replace("-", "");
+
+        // 2. 放入 MDC
+        MDC.put(TRACK_ID, trackId);
+
+        // 3. 响应头返回 traceId
+        HttpServletResponse httpResponse = (HttpServletResponse) response;
+        httpResponse.setHeader(TRACK_ID, trackId);
+
+        try {
+            chain.doFilter(request, response);
+        } finally {
+            MDC.remove(TRACK_ID);
+        }
+    }
+}

+ 0 - 0
fs-hospital/src/main/java/com/fs/framework/config/properties/DruidProperties.java → fs-ad-new-api/src/main/java/com/fs/framework/config/properties/DruidProperties.java


+ 27 - 0
fs-ad-new-api/src/main/java/com/fs/framework/datasource/DynamicDataSource.java

@@ -0,0 +1,27 @@
+package com.fs.framework.datasource;
+
+import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
+
+import javax.sql.DataSource;
+import java.util.Map;
+
+/**
+ * 动态数据源
+ * 
+
+ */
+public class DynamicDataSource extends AbstractRoutingDataSource
+{
+    public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources)
+    {
+        super.setDefaultTargetDataSource(defaultTargetDataSource);
+        super.setTargetDataSources(targetDataSources);
+        super.afterPropertiesSet();
+    }
+
+    @Override
+    protected Object determineCurrentLookupKey()
+    {
+        return DynamicDataSourceContextHolder.getDataSourceType();
+    }
+}

+ 45 - 0
fs-ad-new-api/src/main/java/com/fs/framework/datasource/DynamicDataSourceContextHolder.java

@@ -0,0 +1,45 @@
+package com.fs.framework.datasource;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * 数据源切换处理
+ * 
+
+ */
+public class DynamicDataSourceContextHolder
+{
+    public static final Logger log = LoggerFactory.getLogger(DynamicDataSourceContextHolder.class);
+
+    /**
+     * 使用ThreadLocal维护变量,ThreadLocal为每个使用该变量的线程提供独立的变量副本,
+     *  所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
+     */
+    private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
+
+    /**
+     * 设置数据源的变量
+     */
+    public static void setDataSourceType(String dsType)
+    {
+//        log.info("切换到{}数据源", dsType);
+        CONTEXT_HOLDER.set(dsType);
+    }
+
+    /**
+     * 获得数据源的变量
+     */
+    public static String getDataSourceType()
+    {
+        return CONTEXT_HOLDER.get();
+    }
+
+    /**
+     * 清空数据源变量
+     */
+    public static void clearDataSourceType()
+    {
+        CONTEXT_HOLDER.remove();
+    }
+}

+ 56 - 0
fs-ad-new-api/src/main/java/com/fs/framework/manager/AsyncManager.java

@@ -0,0 +1,56 @@
+package com.fs.framework.manager;
+
+import com.fs.common.utils.Threads;
+import com.fs.common.utils.spring.SpringUtils;
+
+import java.util.TimerTask;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 异步任务管理器
+ * 
+
+ */
+public class AsyncManager
+{
+    /**
+     * 操作延迟10毫秒
+     */
+    private final int OPERATE_DELAY_TIME = 10;
+
+    /**
+     * 异步操作任务调度线程池
+     */
+    private ScheduledExecutorService executor = SpringUtils.getBean("scheduledExecutorService");
+
+    /**
+     * 单例模式
+     */
+    private AsyncManager(){}
+
+    private static AsyncManager me = new AsyncManager();
+
+    public static AsyncManager me()
+    {
+        return me;
+    }
+
+    /**
+     * 执行任务
+     * 
+     * @param task 任务
+     */
+    public void execute(TimerTask task)
+    {
+        executor.schedule(task, OPERATE_DELAY_TIME, TimeUnit.MILLISECONDS);
+    }
+
+    /**
+     * 停止任务线程池
+     */
+    public void shutdown()
+    {
+        Threads.shutdownAndAwaitTermination(executor);
+    }
+}

+ 0 - 0
fs-hospital/src/main/java/com/fs/framework/manager/ShutdownManager.java → fs-ad-new-api/src/main/java/com/fs/framework/manager/ShutdownManager.java


+ 103 - 0
fs-ad-new-api/src/main/java/com/fs/framework/manager/factory/AsyncFactory.java

@@ -0,0 +1,103 @@
+package com.fs.framework.manager.factory;
+
+import com.fs.common.constant.Constants;
+import com.fs.common.utils.LogUtils;
+import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.StringUtils;
+import com.fs.common.utils.ip.AddressUtils;
+import com.fs.common.utils.ip.IpUtils;
+import com.fs.common.utils.spring.SpringUtils;
+import com.fs.system.domain.SysLogininfor;
+import com.fs.system.domain.SysOperLog;
+import com.fs.system.service.ISysLogininforService;
+import com.fs.system.service.ISysOperLogService;
+import eu.bitwalker.useragentutils.UserAgent;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.TimerTask;
+
+/**
+ * 异步工厂(产生任务用)
+ * 
+
+ */
+public class AsyncFactory
+{
+    private static final Logger sys_user_logger = LoggerFactory.getLogger("sys-user");
+
+    /**
+     * 记录登录信息
+     * 
+     * @param username 用户名
+     * @param status 状态
+     * @param message 消息
+     * @param args 列表
+     * @return 任务task
+     */
+    public static TimerTask recordLogininfor(final String username, final String status, final String message,
+            final Object... args)
+    {
+        final UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("User-Agent"));
+        final String ip = IpUtils.getIpAddr(ServletUtils.getRequest());
+        return new TimerTask()
+        {
+            @Override
+            public void run()
+            {
+                String address = AddressUtils.getRealAddressByIP(ip);
+                StringBuilder s = new StringBuilder();
+                s.append(LogUtils.getBlock(ip));
+                s.append(address);
+                s.append(LogUtils.getBlock(username));
+                s.append(LogUtils.getBlock(status));
+                s.append(LogUtils.getBlock(message));
+                // 打印信息到日志
+                sys_user_logger.info(s.toString(), args);
+                // 获取客户端操作系统
+                String os = userAgent.getOperatingSystem().getName();
+                // 获取客户端浏览器
+                String browser = userAgent.getBrowser().getName();
+                // 封装对象
+                SysLogininfor logininfor = new SysLogininfor();
+                logininfor.setUserName(username);
+                logininfor.setIpaddr(ip);
+                logininfor.setLoginLocation(address);
+                logininfor.setBrowser(browser);
+                logininfor.setOs(os);
+                logininfor.setMsg(message);
+                // 日志状态
+                if (StringUtils.equalsAny(status, Constants.LOGIN_SUCCESS, Constants.LOGOUT, Constants.REGISTER))
+                {
+                    logininfor.setStatus(Constants.SUCCESS);
+                }
+                else if (Constants.LOGIN_FAIL.equals(status))
+                {
+                    logininfor.setStatus(Constants.FAIL);
+                }
+                // 插入数据
+                SpringUtils.getBean(ISysLogininforService.class).insertLogininfor(logininfor);
+            }
+        };
+    }
+
+    /**
+     * 操作日志记录
+     * 
+     * @param operLog 操作日志信息
+     * @return 任务task
+     */
+    public static TimerTask recordOper(final SysOperLog operLog)
+    {
+        return new TimerTask()
+        {
+            @Override
+            public void run()
+            {
+                // 远程查询操作地点
+                operLog.setOperLocation(AddressUtils.getRealAddressByIP(operLog.getOperIp()));
+                SpringUtils.getBean(ISysOperLogService.class).insertOperlog(operLog);
+            }
+        };
+    }
+}

+ 1 - 0
fs-ad-new-api/src/main/resources/META-INF/spring-devtools.properties

@@ -0,0 +1 @@
+restart.include.json=/com.alibaba.fastjson.*.jar

+ 10 - 0
fs-ad-new-api/src/main/resources/application.yml

@@ -0,0 +1,10 @@
+# 开发环境配置
+server:
+  port: 7775
+# Spring配置
+spring:
+  profiles:
+    active: dev
+#    active: druid-ylrz
+
+#

+ 0 - 0
fs-hospital/src/main/resources/banner.txt → fs-ad-new-api/src/main/resources/banner.txt


+ 0 - 0
fs-hospital/src/main/resources/i18n/messages.properties → fs-ad-new-api/src/main/resources/i18n/messages.properties


+ 93 - 0
fs-ad-new-api/src/main/resources/logback.xml

@@ -0,0 +1,93 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<configuration>
+    <!-- 日志存放路径 -->
+	<property name="log.path" value="/home/fs-ad-new-api/logs" />
+    <!-- 日志输出格式 -->
+	<property name="log.pattern" value="%d{HH:mm:ss.SSS} [%thread] [%X{X-Track-Id}] %-5level %logger{20} - [%method,%line] - %msg%n" />
+
+	<!-- 控制台输出 -->
+	<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
+		<encoder>
+			<pattern>${log.pattern}</pattern>
+		</encoder>
+	</appender>
+
+	<!-- 系统日志输出 -->
+	<appender name="file_info" class="ch.qos.logback.core.rolling.RollingFileAppender">
+	    <file>${log.path}/sys-info.log</file>
+        <!-- 循环政策:基于时间创建日志文件 -->
+		<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <!-- 日志文件名格式 -->
+			<fileNamePattern>${log.path}/sys-info.%d{yyyy-MM-dd}.log</fileNamePattern>
+			<!-- 日志最大的历史 30 -->
+			<maxHistory>30</maxHistory>
+		</rollingPolicy>
+		<encoder>
+			<pattern>${log.pattern}</pattern>
+		</encoder>
+		<filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <!-- 过滤的级别 -->
+            <level>INFO</level>
+            <!-- 匹配时的操作:接收(记录) -->
+            <onMatch>ACCEPT</onMatch>
+            <!-- 不匹配时的操作:拒绝(不记录) -->
+            <onMismatch>DENY</onMismatch>
+        </filter>
+	</appender>
+
+	<appender name="file_error" class="ch.qos.logback.core.rolling.RollingFileAppender">
+	    <file>${log.path}/sys-error.log</file>
+        <!-- 循环政策:基于时间创建日志文件 -->
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <!-- 日志文件名格式 -->
+            <fileNamePattern>${log.path}/sys-error.%d{yyyy-MM-dd}.log</fileNamePattern>
+			<!-- 日志最大的历史 60天 -->
+			<maxHistory>30</maxHistory>
+        </rollingPolicy>
+        <encoder>
+            <pattern>${log.pattern}</pattern>
+        </encoder>
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <!-- 过滤的级别 -->
+            <level>ERROR</level>
+			<!-- 匹配时的操作:接收(记录) -->
+            <onMatch>ACCEPT</onMatch>
+			<!-- 不匹配时的操作:拒绝(不记录) -->
+            <onMismatch>DENY</onMismatch>
+        </filter>
+    </appender>
+
+	<!-- 用户访问日志输出  -->
+    <appender name="sys-user" class="ch.qos.logback.core.rolling.RollingFileAppender">
+		<file>${log.path}/sys-user.log</file>
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <!-- 按天回滚 daily -->
+            <fileNamePattern>${log.path}/sys-user.%d{yyyy-MM-dd}.log</fileNamePattern>
+            <!-- 日志最大的历史 60天 -->
+            <maxHistory>30</maxHistory>
+        </rollingPolicy>
+        <encoder>
+            <pattern>${log.pattern}</pattern>
+        </encoder>
+    </appender>
+
+	<!-- 系统模块日志级别控制  -->
+	<logger name="com.fs" level="debug" />
+	<!-- Spring日志级别控制  -->
+	<logger name="org.springframework" level="warn" />
+
+	<root level="info">
+		<appender-ref ref="console" />
+	</root>
+
+	<!--系统操作日志-->
+    <root level="info">
+        <appender-ref ref="file_info" />
+        <appender-ref ref="file_error" />
+    </root>
+
+	<!--系统用户操作日志-->
+    <logger name="sys-user" level="info">
+        <appender-ref ref="sys-user"/>
+    </logger>
+</configuration>

+ 15 - 0
fs-ad-new-api/src/main/resources/mybatis/mybatis-config.xml

@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE configuration
+PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-config.dtd">
+<configuration>
+	
+	<settings>
+		<setting name="cacheEnabled"             value="true" />  <!-- 全局映射器启用缓存 -->
+		<setting name="useGeneratedKeys"         value="true" />  <!-- 允许 JDBC 支持自动生成主键 -->
+		<setting name="defaultExecutorType"      value="REUSE" /> <!-- 配置默认的执行器 -->
+		<setting name="logImpl"                  value="SLF4J" /> <!-- 指定 MyBatis 所用日志的具体实现 -->
+		 <setting name="mapUnderscoreToCamelCase" value="true"/>
+	</settings>
+	
+</configuration>

+ 6 - 0
fs-admin/Dockerfile

@@ -0,0 +1,6 @@
+FROM openjdk:8-jre
+# java版本,最好使用openjdk,而不是类似于Java:1.8
+COPY ./target/fs-admin.jar fs-admin.jar
+# 向外暴露的接口,最好与项目yml文件中的端口一致
+ENTRYPOINT ["java","-jar","fs-admin.jar"]
+# 执行启动命令java -jar

+ 4 - 0
fs-admin/pom.xml

@@ -94,6 +94,10 @@
             <artifactId>clickhouse-jdbc</artifactId>
             <version>0.4.6</version>
         </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+        </dependency>
 
     </dependencies>
 

+ 3 - 8
fs-admin/src/main/java/com/fs/FSApplication.java

@@ -1,11 +1,8 @@
 package com.fs;
 
-import com.qiniu.common.Zone;
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
 import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
-import org.springframework.core.io.Resource;
-import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
 import org.springframework.scheduling.annotation.EnableAsync;
 import org.springframework.scheduling.annotation.EnableScheduling;
 import org.springframework.transaction.annotation.Transactional;
@@ -13,14 +10,12 @@ import org.springframework.transaction.annotation.Transactional;
 /**
  * 启动程序
  */
-@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })
+@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
 @Transactional
 @EnableAsync
 @EnableScheduling
-public class FSApplication
-{
-    public static void main(String[] args)
-    {
+public class FSApplication {
+    public static void main(String[] args) {
         // System.setProperty("spring.devtools.restart.enabled", "false");
         SpringApplication.run(FSApplication.class, args);
         System.out.println("admin启动成功");

+ 706 - 36
fs-admin/src/main/java/com/fs/api/controller/IndexStatisticsController.java

@@ -1,16 +1,31 @@
 package com.fs.api.controller;
 
+import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
 import com.fs.common.core.domain.R;
+import com.fs.common.core.domain.entity.SysDept;
 import com.fs.common.core.redis.RedisCache;
+import com.fs.company.constant.CompanyTrafficConstants;
+import com.fs.company.domain.Company;
+import com.fs.company.service.ICompanyService;
+import com.fs.his.utils.ConfigUtil;
+import com.fs.hisStore.config.MedicalMallConfig;
 import com.fs.statis.StatisticsRedisConstant;
 import com.fs.statis.dto.*;
+import com.fs.statis.param.StatisticsDeptCompanyParam;
+import com.fs.statis.service.IStatisticsService;
 import com.fs.system.domain.SysConfig;
 import com.fs.system.service.ISysConfigService;
+import com.fs.system.service.ISysDeptService;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
 
-import java.util.ArrayList;
-import java.util.List;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.YearMonth;
+import java.util.*;
+import java.util.function.BinaryOperator;
+import java.util.stream.Collectors;
 
 import static com.fs.statis.StatisticsRedisConstant.*;
 
@@ -19,18 +34,34 @@ import static com.fs.statis.StatisticsRedisConstant.*;
  */
 @RestController
 @RequestMapping("/index/statistics")
+@Slf4j
 public class IndexStatisticsController {
     @Autowired
     private RedisCache redisCache;
 
     @Autowired
     private ISysConfigService sysConfigService;
+
+    @Autowired
+    private ConfigUtil configUtil;
+
+    @Autowired
+    private ISysDeptService deptService;
+
+    @Autowired
+    private ICompanyService companyService;
+
+    @Autowired
+    private MedicalMallConfig medicalMallConfig;
+
+    @Autowired
+    private IStatisticsService statisticsService;
     /**
      * 分析概览
      */
     @PostMapping("/analysisPreview")
     public R analysisPreview(@RequestBody AnalysisPreviewQueryDTO param){
-        AnalysisPreviewDTO analysisPreviewDTO = null;
+        AnalysisPreviewDTO analysisPreviewDTO = new AnalysisPreviewDTO();
         Integer type = param.getType();
         Integer userType = param.getUserType();
 
@@ -41,7 +72,69 @@ public class IndexStatisticsController {
         if(userType == null) {
             userType = 0;
         }
-        analysisPreviewDTO = redisCache.getCacheObject(String.format("%s:%d:%d",DATA_OVERVIEW_DEALER_ANALYSISPREVIEW,type,userType));
+        if(medicalMallConfig.isStatics()|| (param.getCompanyId() == null && param.getDeptId() == null) || (param.getCompanyId() == null && param.getDeptId() == 1)){
+            analysisPreviewDTO = redisCache.getCacheObject(String.format("%s:%d:%d",DATA_OVERVIEW_DEALER_ANALYSISPREVIEW,type,userType));
+        }else if(param.getCompanyId() != null){
+            analysisPreviewDTO = redisCache.getCacheObject(String.format("%s:%d:%d:%d",DATA_OVERVIEW_DEALER_ANALYSISPREVIEW,type,userType,param.getCompanyId()));
+        }else{
+            Company company = new Company();
+            company.setDeptId(param.getDeptId());
+            Long[] companyIds = companyService.selectCompanyList(company).stream().map(Company::getCompanyId).toArray(Long[]::new);
+            for(Long companyId : companyIds){
+                AnalysisPreviewDTO clildDTO = redisCache.getCacheObject(String.format("%s:%d:%d:%d",DATA_OVERVIEW_DEALER_ANALYSISPREVIEW,type,userType,companyId));
+                if (clildDTO != null) {
+                    //将除了completedRate、correctRate和watchRate之外的数据相加,并计算completedRate、correctRate和watchRate
+                    analysisPreviewDTO.setWatchUserCount(
+                            analysisPreviewDTO.getWatchUserCount() == 0 ? 0 : analysisPreviewDTO.getWatchUserCount()
+                                +(clildDTO.getWatchUserCount() == 0 ? 0 : clildDTO.getWatchUserCount())
+                    );
+                    analysisPreviewDTO.setCompletedUserCount(
+                            analysisPreviewDTO.getCompletedUserCount() == 0 ? 0 : analysisPreviewDTO.getCompletedUserCount()
+                                +(clildDTO.getCompletedUserCount() == 0 ? 0 : clildDTO.getCompletedUserCount())
+                    );
+                    analysisPreviewDTO.setCompletedCount(
+                            analysisPreviewDTO.getCompletedCount() == 0 ? 0 : analysisPreviewDTO.getCompletedCount()
+                                +(clildDTO.getCompletedCount() == 0 ? 0 : clildDTO.getCompletedCount())
+                    );
+                    analysisPreviewDTO.setWatchCount(
+                            analysisPreviewDTO.getWatchCount() == 0 ? 0 : analysisPreviewDTO.getWatchCount()
+                                +(clildDTO.getWatchCount() == 0 ? 0 : clildDTO.getWatchCount())
+                    );
+                    analysisPreviewDTO.setAnswerMemberCount(
+                            analysisPreviewDTO.getAnswerMemberCount() == 0 ? 0 : analysisPreviewDTO.getAnswerMemberCount()
+                                +(clildDTO.getAnswerMemberCount() == 0 ? 0 : clildDTO.getAnswerMemberCount())
+                    );
+                    analysisPreviewDTO.setCorrectUserCount(
+                            analysisPreviewDTO.getCorrectUserCount() == 0 ? 0 : analysisPreviewDTO.getCorrectUserCount()
+                                +(clildDTO.getCorrectUserCount() == 0 ? 0 : clildDTO.getCorrectUserCount())
+                    );
+                    analysisPreviewDTO.setRewardCount(
+                            analysisPreviewDTO.getRewardCount() == 0 ? 0 : analysisPreviewDTO.getRewardCount()
+                                +(clildDTO.getRewardCount() == 0 ? 0 : clildDTO.getRewardCount())
+                    );
+                    analysisPreviewDTO.setRewardMoney(
+                            analysisPreviewDTO.getRewardMoney() == null ? BigDecimal.ZERO : analysisPreviewDTO.getRewardMoney()
+                                .add(clildDTO.getRewardMoney() == null ? BigDecimal.ZERO : clildDTO.getRewardMoney())
+                    );
+                }
+            }
+            if(analysisPreviewDTO.getAnswerMemberCount() != 0){
+                analysisPreviewDTO.setCorrectRate(
+                        analysisPreviewDTO.getCorrectUserCount() == 0 ? "0" : String.format("%.2f", analysisPreviewDTO.getCorrectUserCount() * 100.0 / analysisPreviewDTO.getAnswerMemberCount())
+                );
+            }
+            if(analysisPreviewDTO.getWatchUserCount() != 0){
+                analysisPreviewDTO.setCompletedRate(
+                        analysisPreviewDTO.getCompletedUserCount() == 0 ? "0" : String.format("%.2f", analysisPreviewDTO.getCompletedUserCount() * 100.0 / analysisPreviewDTO.getWatchUserCount())
+                );
+            }
+            if(analysisPreviewDTO.getWatchCount() != 0){
+                analysisPreviewDTO.setWatchRate(
+                        analysisPreviewDTO.getCompletedCount() == 0 ? "0" : String.format("%.2f", analysisPreviewDTO.getCompletedCount() * 100.0 / analysisPreviewDTO.getWatchCount())
+                );
+            }
+
+        }
 
         return R.ok().put("data",analysisPreviewDTO);
     }
@@ -51,9 +144,40 @@ public class IndexStatisticsController {
      * 消费余额
      */
     @GetMapping("/rechargeComsumption")
-    public R rechargeComsumption(){
-        ConsumptionBalanceDataDTO consumptionBalanceDataDTO = redisCache.getCacheObject(StatisticsRedisConstant.DATA_OVERVIEW_DEALER_BALANCE);
+    public R rechargeComsumption(StatisticsDeptCompanyParam param){
+        ConsumptionBalanceDataDTO consumptionBalanceDataDTO = new ConsumptionBalanceDataDTO();
+        if(medicalMallConfig.isStatics() || (param.getCompanyId() == null && param.getDeptId() == null) || (param.getCompanyId() == null && param.getDeptId() == 1)) {
+            consumptionBalanceDataDTO = redisCache.getCacheObject(StatisticsRedisConstant.DATA_OVERVIEW_DEALER_BALANCE);
+        }else if(param.getCompanyId() != null){
+            consumptionBalanceDataDTO = redisCache.getCacheObject(String.format("%s:%d",DATA_OVERVIEW_DEALER_BALANCE,param.getCompanyId()));
+        }else{
+            Company company = new Company();
+            company.setDeptId(param.getDeptId());
+            Long[] companyIds = companyService.selectCompanyList(company).stream().map(Company::getCompanyId).toArray(Long[]::new);
+            for(Long companyId : companyIds){
+                ConsumptionBalanceDataDTO clildDTO = redisCache.getCacheObject(String.format("%s:%d",DATA_OVERVIEW_DEALER_BALANCE,companyId));
+                consumptionBalanceDataDTO.setBalance(
+                    (consumptionBalanceDataDTO.getBalance() == null ? BigDecimal.ZERO : consumptionBalanceDataDTO.getBalance())
+                    .add(clildDTO.getBalance() == null ? BigDecimal.ZERO : clildDTO.getBalance())
+                );
+                consumptionBalanceDataDTO.setTodayComsumption(
+                    (consumptionBalanceDataDTO.getTodayComsumption() == null ? BigDecimal.ZERO : consumptionBalanceDataDTO.getTodayComsumption())
+                    .add(clildDTO.getTodayComsumption() == null ? BigDecimal.ZERO : clildDTO.getTodayComsumption())
+                );
+                consumptionBalanceDataDTO.setYesterdayComsumption(
+                    (consumptionBalanceDataDTO.getYesterdayComsumption() == null ? BigDecimal.ZERO : consumptionBalanceDataDTO.getYesterdayComsumption())
+                    .add(clildDTO.getYesterdayComsumption() == null ? BigDecimal.ZERO : clildDTO.getYesterdayComsumption())
+                );
+            }
+        }
+        BigDecimal redPacketCompanyMoney = redisCache.getCacheObject("redpacket_money");
+        if(ObjectUtils.isNull(redPacketCompanyMoney)){
+            redPacketCompanyMoney = BigDecimal.ZERO;
+        }
+        if (consumptionBalanceDataDTO != null){
+            consumptionBalanceDataDTO.setRunTianBalance(redPacketCompanyMoney);
 
+        }
         return R.ok().put("data", consumptionBalanceDataDTO);
     }
 
@@ -62,15 +186,58 @@ public class IndexStatisticsController {
      * @return
      */
     @GetMapping("/trafficLog")
-    public R getTrafficLog(){
-        TrafficLogDTO trafficLogDTO = redisCache.getCacheObject(DATA_OVERVIEW_TRAFFIC_LOG);
-        SysConfig sysConfig = sysConfigService.selectConfigByConfigKey("redPacket.Traffic.config");
-        if(trafficLogDTO == null || sysConfig == null) {
-            return null;
+    public R getTrafficLog(StatisticsDeptCompanyParam  param){
+        TrafficLogDTO result = new TrafficLogDTO();
+        if(!medicalMallConfig.isStatics() || (param.getCompanyId() == null && param.getDeptId() == null) || (param.getCompanyId() == null && param.getDeptId() == 1)) {
+            result = redisCache.getCacheObject(DATA_OVERVIEW_TRAFFIC_LOG);
+            if (!medicalMallConfig.isStatics()) {
+                SysConfig sysConfig = sysConfigService.selectConfigByConfigKey("redPacket.Traffic.config");
+                if(result == null || sysConfig == null) {
+                    return null;
+                }
+                String configValue = sysConfig.getConfigValue();
+                result.setTraffic(configValue);
+            } else {
+                //所有部门流量之和
+                SysDept dept = new SysDept();
+                dept.setParentId(1L);
+                Long[] deptIds = deptService.selectDeptList(dept).stream().map(SysDept::getDeptId).toArray(Long[]::new);
+                long trafficTemp = 0L;
+                for(Long deptId : deptIds){
+                    Object clildTraffic = redisCache.getCacheObject(CompanyTrafficConstants.CACHE_KEY+":"+deptId);
+                    if(clildTraffic != null){
+                        trafficTemp += Long.parseLong(clildTraffic.toString());
+                    }
+                }
+                result.setTraffic(String.format("%.2f",trafficTemp * 1024.0));
+            }
+        }else if(param.getCompanyId() != null){
+            Company company = companyService.selectCompanyById(param.getCompanyId());
+            getTrafficLogResult(result,CompanyTrafficConstants.CACHE_KEY+":"+company.getDeptId()+":"+param.getCompanyId());
+        }else{
+            getTrafficLogResult(result,CompanyTrafficConstants.CACHE_KEY+":"+param.getDeptId());
         }
-        String configValue = sysConfig.getConfigValue();
-        trafficLogDTO.setTraffic(configValue);
-        return R.ok().put("data",trafficLogDTO);
+        return R.ok().put("data",result);
+    }
+
+    private void getTrafficLogResult(TrafficLogDTO result, String key){
+        //昨天
+        LocalDate yesterday = LocalDate.now().minusDays(1);
+        Object yesterdayCount = redisCache.getCacheObject(key+":"+yesterday);
+        Object todayCount = redisCache.getCacheObject(key+":"+LocalDate.now());
+        Object thisMonthCount = redisCache.getCacheObject(key+":"+YearMonth.now());
+        Object traffic = redisCache.getCacheObject(key);
+        result.setYesterday(parseRedisNumberValueToLong(yesterdayCount));
+        //今天
+        result.setToday(parseRedisNumberValueToLong(todayCount));
+        //本月
+        result.setThisMonth(parseRedisNumberValueToLong(thisMonthCount));
+        //剩余
+        result.setTraffic(traffic == null?"0":traffic.toString());
+    }
+
+    private Long parseRedisNumberValueToLong(Object value){
+        return value == null ? 0 : Long.parseLong(value.toString());
     }
 
     /**
@@ -87,9 +254,73 @@ public class IndexStatisticsController {
         if(userType == null){
             userType = 0;
         }
-        String key = String.format("%s:%d:%d", DATA_OVERVIEW_DEALER_CHARTS, type,userType);
-        List<DeaMemberTopTenDTO> deaMemberTopTenDTOS = redisCache.getCacheObject(key);
-        return R.ok().put("data", deaMemberTopTenDTOS);
+        List<WatchEndPlayTrendDTO> watchEndPlayTrendDTOS;
+        // 参考watchCourseTopTen方法的处理逻辑
+        if (!medicalMallConfig.isStatics() || (param.getCompanyId() == null && param.getDeptId() == null) || (param.getCompanyId() == null && param.getDeptId() == 1)){
+            String key = String.format("%s:%d:%d", DATA_OVERVIEW_DEALER_CHARTS, type,userType);
+            watchEndPlayTrendDTOS = redisCache.getCacheObject(key);
+        }else if(param.getCompanyId() != null){
+            String key = String.format("%s:%d:%d:%d", DATA_OVERVIEW_DEALER_CHARTS, type,userType,param.getCompanyId());
+            watchEndPlayTrendDTOS = redisCache.getCacheObject(key);
+        }else{
+            Company company = new Company();
+            company.setDeptId(param.getDeptId());
+            Long[] companyIds = companyService.selectCompanyList(company).stream().map(Company::getCompanyId).toArray(Long[]::new);
+            List<WatchEndPlayTrendDTO> tempDTOS = new ArrayList<>();
+            for(Long companyId : companyIds){
+                String key = String.format("%s:%d:%d:%d", DATA_OVERVIEW_DEALER_CHARTS, type,userType,companyId);
+                List<WatchEndPlayTrendDTO> companyData = redisCache.getCacheObject(key);
+                if (companyData != null) {
+                    tempDTOS.addAll(companyData);
+                }
+            }
+            // 根据startDate 和 x 分组,合并watchUserCount和completedUserCount 限制最多返回10条记录
+            watchEndPlayTrendDTOS = tempDTOS.stream()
+                    .collect(Collectors.groupingBy(
+                            dto -> dto.getStartDate() + ":" + dto.getX(),  // 根据startDate和x分组
+                            Collectors.reducing(new WatchEndPlayTrendDTO(), (dto1, dto2) -> {
+                                // 合并watchUserCount和completedUserCount
+                                WatchEndPlayTrendDTO result = new WatchEndPlayTrendDTO();
+                                // 复制分组标识字段
+                                if (dto2 != null && dto2.getStartDate() != null) {
+                                    result.setStartDate(dto2.getStartDate());
+                                } else if (dto1 != null) {
+                                    result.setStartDate(dto1.getStartDate());
+                                }
+
+                                if (dto2 != null && dto2.getX() != null) {
+                                    result.setX(dto2.getX());
+                                } else if (dto1 != null) {
+                                    result.setX(dto1.getX());
+                                }
+
+                                // 合并数值字段
+                                result.setWatchUserCount(
+                                        (dto1 == null || dto1.getWatchUserCount() == null ? 0 : dto1.getWatchUserCount()) +
+                                                (dto2 == null || dto2.getWatchUserCount() == null ? 0 : dto2.getWatchUserCount())
+                                );
+
+                                result.setCompletedUserCount(
+                                        (dto1 == null || dto1.getCompletedUserCount() == null ? 0 : dto1.getCompletedUserCount()) +
+                                                (dto2 == null || dto2.getCompletedUserCount() == null ? 0 : dto2.getCompletedUserCount())
+                                );
+
+                                return result;
+                            })
+                    ))
+                    .values()
+                    .stream()
+                    .filter(Objects::nonNull)  // 过滤掉null值
+                    .limit(10)
+                    .sorted(Comparator.comparing(WatchEndPlayTrendDTO::getX))
+                    .collect(Collectors.toList());
+        }
+
+        if(watchEndPlayTrendDTOS == null){
+            watchEndPlayTrendDTOS = new ArrayList<>();
+        }
+
+        return R.ok().put("data", watchEndPlayTrendDTOS);
     }
 
     /**
@@ -107,8 +338,49 @@ public class IndexStatisticsController {
         if(userType == null){
             userType = 0;
         }
+        List<DeaMemberTopTenDTO> deaMemberTopTenDTOS = new ArrayList<>();
+        // 参考deaMemberTopTen方法处理逻辑
+        if (!medicalMallConfig.isStatics() || (param.getCompanyId() == null && param.getDeptId() == null) || (param.getCompanyId() == null && param.getDeptId() == 1)){
+            String key = String.format("%s:%d:%d:%d", CHARTS_MEMBER_TOP_TEN_WATCH, type, statisticalType,userType);
+            deaMemberTopTenDTOS =  redisCache.getCacheObject(key);
+        }else if(param.getCompanyId() != null){
+            String key = String.format("%s:%d:%d:%d:%d", CHARTS_MEMBER_TOP_TEN_WATCH, type, statisticalType,userType,param.getCompanyId());
+            deaMemberTopTenDTOS = redisCache.getCacheObject(key);
+        }else{
+            Company company = new Company();
+            company.setDeptId(param.getDeptId());
+            Long[] companyIds = companyService.selectCompanyList(company).stream().map(Company::getCompanyId).toArray(Long[]::new);
+            List<DeaMemberTopTenDTO> tempDTOS = new ArrayList<>();
+            for(Long companyId : companyIds){
+                String key = String.format("%s:%d:%d:%d:%d", CHARTS_MEMBER_TOP_TEN_WATCH, type, statisticalType,userType,companyId);
+                List<DeaMemberTopTenDTO> companyData = redisCache.getCacheObject(key);
+                if (companyData != null) {
+                    tempDTOS.addAll(companyData);
+                }
+            }
+            // companyId 和 companyName 分组,合并watchUserCount 限制最多返回10条记录
+            deaMemberTopTenDTOS = tempDTOS.stream()
+                    .collect(Collectors.groupingBy(
+                            dto -> dto.getCompanyId() + ":" + dto.getCompanyName(),  // 根据companyId和companyName分组
+                            Collectors.reducing(new DeaMemberTopTenDTO(), (dto1, dto2) -> {
+                                DeaMemberTopTenDTO result = new DeaMemberTopTenDTO();
+                                result.setCompanyId(dto1.getCompanyId());
+                                result.setCompanyName(dto1.getCompanyName());
+                                result.setWatchUserCount(
+                                        (dto1.getWatchUserCount() == null ? 0 : dto1.getWatchUserCount()) +
+                                                (dto2 == null || dto2.getWatchUserCount() == null ? 0 : dto2.getWatchUserCount())
+                                );
+                                return result;
+                            })
+                    ))
+                    .values()
+                    .stream()
+                    .filter(Objects::nonNull)  // 过滤掉null值
+                    .limit(10)
+                    .sorted(Comparator.comparing(DeaMemberTopTenDTO::getWatchUserCount, Comparator.nullsLast(Comparator.reverseOrder())))
+                    .collect(Collectors.toList());
+        }
 
-        List<DeaMemberTopTenDTO> deaMemberTopTenDTOS = redisCache.getCacheObject(String.format("%s:%d:%d:%d", CHARTS_MEMBER_TOP_TEN_WATCH, type, statisticalType,userType));
         if(deaMemberTopTenDTOS == null){
             deaMemberTopTenDTOS = new ArrayList<>();
         }
@@ -123,8 +395,50 @@ public class IndexStatisticsController {
         Integer type = param.getType();
         Integer dataType = param.getDataType();
         Integer userType = param.getUserType();
+        List<RewardMoneyTopTenDTO> rewardMoneyTopTenDTOS = new ArrayList<>();
+        // 参考rewardMoneyTopTen方法处理逻辑
+        if(!medicalMallConfig.isStatics() || (param.getCompanyId() == null && param.getDeptId() == null) || (param.getCompanyId() == null && param.getDeptId() == 1)){
+            String key = String.format("%s:%d:%d:%d", CHARTS_REWARD_MONEY_TOP_TEN, type,dataType,userType);
+            rewardMoneyTopTenDTOS = redisCache.getCacheObject(key);
+        }else if(param.getCompanyId() != null){
+            String key = String.format("%s:%d:%d:%d:%d", CHARTS_REWARD_MONEY_TOP_TEN, type,dataType,userType,param.getCompanyId());
+            rewardMoneyTopTenDTOS = redisCache.getCacheObject(key);
+        }else{
+            Company company = new Company();
+            company.setDeptId(param.getDeptId());
+            Long[] companyIds = companyService.selectCompanyList(company).stream().map(Company::getCompanyId).toArray(Long[]::new);
+            List<RewardMoneyTopTenDTO> tempDTOS = new ArrayList<>();
+            for(Long companyId : companyIds){
+                String key = String.format("%s:%d:%d:%d:%d", CHARTS_REWARD_MONEY_TOP_TEN, type,dataType,userType,companyId);
+                List<RewardMoneyTopTenDTO> companyData = redisCache.getCacheObject(key);
+                if (companyData != null) {
+                    tempDTOS.addAll(companyData);
+                }
+            }
+            rewardMoneyTopTenDTOS = tempDTOS.stream()
+                    .collect(Collectors.groupingBy(
+                            dto -> dto.getCompanyId() + ":" + dto.getCompanyName(),  // 根据companyId和companyName分组
+                            Collectors.reducing(new RewardMoneyTopTenDTO(), (dto1, dto2) -> {
+                                RewardMoneyTopTenDTO result = new RewardMoneyTopTenDTO();
+                                result.setCompanyId(dto1.getCompanyId());
+                                result.setCompanyName(dto1.getCompanyName());
+                                result.setRewardMoney(
+                                        (dto1.getRewardMoney() == null ? BigDecimal.ZERO : dto1.getRewardMoney())
+                                        .add(
+                                                (dto2 == null || dto2.getRewardMoney() == null ? BigDecimal.ZERO : dto2.getRewardMoney())
+                                        )
+                                );
+                                return result;
+                            })
+                    ))
+                    .values()
+                    .stream()
+                    .filter(Objects::nonNull)  // 过滤掉null值
+                    .limit(10)
+                    .sorted(Comparator.comparing(RewardMoneyTopTenDTO::getRewardMoney, Comparator.nullsLast(Comparator.reverseOrder())))
+                    .collect(Collectors.toList());
+        }
 
-        List<RewardMoneyTopTenDTO> rewardMoneyTopTenDTOS = redisCache.getCacheObject( String.format("%s:%d:%d:%d", CHARTS_REWARD_MONEY_TOP_TEN, type,dataType,userType));
         return R.ok().put("data", rewardMoneyTopTenDTOS);
     }
 
@@ -135,7 +449,47 @@ public class IndexStatisticsController {
     public R rewardMoneyTrend(@RequestBody AnalysisPreviewQueryDTO param){
         Integer type = param.getType();
         Integer userType = param.getUserType();
-        List<RewardMoneyTrendDTO> rewardMoneyTrendDTOS = redisCache.getCacheObject( String.format("%s:%d:%d", CHARTS_REWARD_MONEY_TREND, type,userType));
+        List<RewardMoneyTrendDTO> rewardMoneyTrendDTOS = new ArrayList<>();
+        // 参考rewardMoneyTrend方法处理逻辑
+        if(!medicalMallConfig.isStatics() || (param.getCompanyId() == null && param.getDeptId() == null) || (param.getCompanyId() == null && param.getDeptId() == 1)){
+            String key = String.format("%s:%d:%d", CHARTS_REWARD_MONEY_TREND, type,userType);
+            rewardMoneyTrendDTOS = redisCache.getCacheObject(key);
+        }else if(param.getCompanyId() != null){
+            String key = String.format("%s:%d:%d:%d", CHARTS_REWARD_MONEY_TREND, type,userType,param.getCompanyId());
+            rewardMoneyTrendDTOS = redisCache.getCacheObject(key);
+        }else{
+            Company company = new Company();
+            company.setDeptId(param.getDeptId());
+            Long[] companyIds = companyService.selectCompanyList(company).stream().map(Company::getCompanyId).toArray(Long[]::new);
+            List<RewardMoneyTrendDTO> tempDTOS = new ArrayList<>();
+            for(Long companyId : companyIds){
+                String key = String.format("%s:%d:%d:%d", CHARTS_REWARD_MONEY_TREND, type,userType,companyId);
+                List<RewardMoneyTrendDTO> companyData = redisCache.getCacheObject(key);
+                if (companyData != null) {
+                    tempDTOS.addAll(companyData);
+                }
+            }
+            //根据startDate和x分组,合并rewardMoney,根据x排序,限制10
+            rewardMoneyTrendDTOS = tempDTOS.stream()
+                    .collect(Collectors.groupingBy(dto -> dto.getStartDate() + ":" + dto.getX(),
+                            Collectors.reducing(new RewardMoneyTrendDTO(), (dto1, dto2) -> {
+                        RewardMoneyTrendDTO result = new RewardMoneyTrendDTO();
+                        result.setStartDate(dto1.getStartDate());
+                        result.setX(dto1.getX());
+                        result.setRewardMoney(
+                                (dto1.getRewardMoney() == null ? BigDecimal.ZERO : dto1.getRewardMoney())
+                                .add(
+                                        (dto2 == null || dto2.getRewardMoney() == null ? BigDecimal.ZERO : dto2.getRewardMoney())
+                                )
+                        );
+                        return result;
+                    })))
+                    .values()
+                    .stream()
+                    .sorted(Comparator.comparing(RewardMoneyTrendDTO::getX))
+                    .limit(10)
+                    .collect(Collectors.toList());
+        }
         return R.ok().put("data", rewardMoneyTrendDTOS);
     }
 
@@ -148,29 +502,196 @@ public class IndexStatisticsController {
         String sort = param.getSort();
         Integer statisticalType = param.getStatisticalType();
         Integer userType = param.getUserType();
+        List<CourseStatsDTO> courseStatsDTOS;
+        if (!medicalMallConfig.isStatics() || (param.getCompanyId() == null && param.getDeptId() == null) || (param.getCompanyId() == null && param.getDeptId() == 1)){
+            courseStatsDTOS = redisCache.getCacheObject(String.format("%s:%d:%d:%d:%s", CHARTS_WATCH_TOP_TEN, type,statisticalType,userType,sort));
+        }else if(param.getCompanyId() != null){
+            courseStatsDTOS = redisCache.getCacheObject(String.format("%s:%d:%d:%d:%s:%d", CHARTS_WATCH_TOP_TEN, type,statisticalType,userType,sort,param.getCompanyId()));
+        }else{
+            Company company = new Company();
+            company.setDeptId(param.getDeptId());
+            Long[] companyIds = companyService.selectCompanyList(company).stream().map(Company::getCompanyId).toArray(Long[]::new);
+            List<CourseStatsDTO> tempDTOS = new ArrayList<>();
+            for(Long companyId : companyIds){
+                List<CourseStatsDTO> companyDTO = redisCache.getCacheObject(String.format("%s:%d:%d:%d:%s:%d", CHARTS_WATCH_TOP_TEN, type,statisticalType,userType,sort,companyId));
+                if (companyDTO != null) {
+                    tempDTOS.addAll(companyDTO);
+                }
+            }
+            // courseId和courseName分组,合并watchUserCount、completedUserCount、answerUserCount、correctUserCount 限制最多返回10条记录
+            courseStatsDTOS = tempDTOS.stream()
+                    .collect(Collectors.groupingBy(dto -> dto.getCourseId() + ":" + dto.getCourseName()))
+                    .values()
+                    .stream()
+                    .map(group -> {
+                        if (group.isEmpty()) {
+                            return null;
+                        }
+
+                        // 取第一个作为基础对象
+                        CourseStatsDTO result = new CourseStatsDTO();
+                        CourseStatsDTO first = group.get(0);
+                        result.setCourseId(first.getCourseId());
+                        result.setCourseName(first.getCourseName());
+
+                        // 合并所有数值字段
+                        int watchUserCount = 0;
+                        int completedUserCount = 0;
+                        int answerUserCount = 0;
+                        int correctUserCount = 0;
+
+                        for (CourseStatsDTO dto : group) {
+                            watchUserCount += (dto.getWatchUserCount() == null ? 0 : dto.getWatchUserCount());
+                            completedUserCount += (dto.getCompletedUserCount() == null ? 0 : dto.getCompletedUserCount());
+                            answerUserCount += (dto.getAnswerUserCount() == null ? 0 : dto.getAnswerUserCount());
+                            correctUserCount += (dto.getCorrectUserCount() == null ? 0 : dto.getCorrectUserCount());
+                        }
+
+                        result.setWatchUserCount(watchUserCount);
+                        result.setCompletedUserCount(completedUserCount);
+                        result.setAnswerUserCount(answerUserCount);
+                        result.setCorrectUserCount(correctUserCount);
+
+                        return result;
+                    })
+                    .filter(Objects::nonNull)
+                    .sorted(Comparator.comparing(CourseStatsDTO::getWatchUserCount, Comparator.nullsLast(Comparator.reverseOrder())))
+                    .limit(10)
+                    .collect(Collectors.toList());
+
+        }
 
-        List<CourseStatsDTO> courseStatsDTOS = redisCache.getCacheObject(String.format("%s:%d:%d:%d:%s", CHARTS_WATCH_TOP_TEN, type,statisticalType,userType,sort));
         return R.ok().put("data", courseStatsDTOS);
     }
 
+    private List<CourseStatsDTO> groupAndSumCourseStatsWithStream(List<CourseStatsDTO> courseStatsList) {
+        // 定义合并逻辑
+        BinaryOperator<CourseStatsDTO> mergeFunction = (s1, s2) -> {
+            s1.setWatchUserCount(
+                    (s1.getWatchUserCount() == null ? 0 : s1.getWatchUserCount()) +
+                            (s2.getWatchUserCount() == null ? 0 : s2.getWatchUserCount())
+            );
+
+            s1.setCompletedUserCount(
+                    (s1.getCompletedUserCount() == null ? 0 : s1.getCompletedUserCount()) +
+                            (s2.getCompletedUserCount() == null ? 0 : s2.getCompletedUserCount())
+            );
+
+            s1.setAnswerUserCount(
+                    (s1.getAnswerUserCount() == null ? 0 : s1.getAnswerUserCount()) +
+                            (s2.getAnswerUserCount() == null ? 0 : s2.getAnswerUserCount())
+            );
+
+            s1.setCorrectUserCount(
+                    (s1.getCorrectUserCount() == null ? 0 : s1.getCorrectUserCount()) +
+                            (s2.getCorrectUserCount() == null ? 0 : s2.getCorrectUserCount())
+            );
+
+            return s1;
+        };
+
+        // 分组、合并并排序、限制10条
+        return courseStatsList.stream()
+                .collect(Collectors.toMap(
+                        CourseStatsDTO::getCourseId,
+                        dto -> {
+                            CourseStatsDTO copy = new CourseStatsDTO();
+                            copy.setCourseId(dto.getCourseId());
+                            copy.setCourseName(dto.getCourseName());
+                            copy.setWatchUserCount(dto.getWatchUserCount() == null ? 0 : dto.getWatchUserCount());
+                            copy.setCompletedUserCount(dto.getCompletedUserCount() == null ? 0 : dto.getCompletedUserCount());
+                            copy.setAnswerUserCount(dto.getAnswerUserCount() == null ? 0 : dto.getAnswerUserCount());
+                            copy.setCorrectUserCount(dto.getCorrectUserCount() == null ? 0 : dto.getCorrectUserCount());
+                            return copy;
+                        },
+                        mergeFunction,
+                        LinkedHashMap::new
+                ))
+                .values()
+                .stream()
+                .sorted(Comparator.comparing(CourseStatsDTO::getWatchUserCount, Comparator.nullsLast(Comparator.reverseOrder())))
+                .limit(10)
+                .collect(Collectors.toList());
+    }
+
     /**
      * 数据概览
      */
     @GetMapping("/dealerAggregated")
-    public R dealerAggregated(){
-        DealerAggregatedDTO dealerAggregatedDTO = redisCache.getCacheObject(StatisticsRedisConstant.DATA_OVERVIEW_DEALER_AGGREGATED);
+    public R dealerAggregated(StatisticsDeptCompanyParam param){
+        DealerAggregatedDTO result = new DealerAggregatedDTO();
+        if (!medicalMallConfig.isStatics() || (param.getCompanyId() == null && param.getDeptId() == null) || (param.getCompanyId() == null && param.getDeptId() == 1)) {
+            result = redisCache.getCacheObject(StatisticsRedisConstant.DATA_OVERVIEW_DEALER_AGGREGATED);
+        }else if (param.getCompanyId() != null) {
+            result = redisCache.getCacheObject(String.format("%s:%d", DATA_OVERVIEW_DEALER_AGGREGATED, param.getCompanyId()));
+        //没选中销售公司,部门不为总公司
+        }else{
+            //Long padMaxNum = deptLimiteService.selectById(param.getDeptId()).getMaxPadNum();
+            Company company = new Company();
+            company.setDeptId(param.getDeptId());
+            Long[] companyIds = companyService.selectCompanyList(company).stream().map(Company::getCompanyId).toArray(Long[]::new);
+            for(Long companyId : companyIds) {
+                DealerAggregatedDTO dealerAggregatedDTO = redisCache.getCacheObject(String.format("%s:%d", DATA_OVERVIEW_DEALER_AGGREGATED, companyId));
+                // 添加空值检查
+                if (dealerAggregatedDTO != null) {
+                    result.setDealderCount((result.getDealderCount() == null ? 0 : result.getDealderCount())
+                            + (dealerAggregatedDTO.getDealderCount() == null ? 0 : dealerAggregatedDTO.getDealderCount()));
+                    result.setGroupMgrCount((result.getGroupMgrCount() == null ? 0 : result.getGroupMgrCount())
+                            + (dealerAggregatedDTO.getGroupMgrCount() == null ? 0 : dealerAggregatedDTO.getGroupMgrCount()));
+                    result.setMemberCount((result.getMemberCount() == null ? 0 : result.getMemberCount())
+                            + (dealerAggregatedDTO.getMemberCount() == null ? 0 : dealerAggregatedDTO.getMemberCount()));
+                    result.setNormalNum((result.getNormalNum() == null ? 0 : result.getNormalNum())
+                            + (dealerAggregatedDTO.getNormalNum() == null ? 0 : dealerAggregatedDTO.getNormalNum()));
+                    result.setBlackNum((result.getBlackNum() == null ? 0 : result.getBlackNum())
+                            + (dealerAggregatedDTO.getBlackNum() == null ? 0 : dealerAggregatedDTO.getBlackNum()));
+                    result.setQwMemberNum((result.getQwMemberNum() == null ? 0 : result.getQwMemberNum())
+                            + (dealerAggregatedDTO.getQwMemberNum() == null ? 0 : dealerAggregatedDTO.getQwMemberNum()));
+                    result.setTodayIncreaseUserNum((result.getTodayIncreaseUserNum() == null ? 0 : result.getTodayIncreaseUserNum())
+                            + (dealerAggregatedDTO.getTodayIncreaseUserNum() == null ? 0 : dealerAggregatedDTO.getTodayIncreaseUserNum()));
+                    result.setOrderTotalNum((result.getOrderTotalNum() == null ? 0 : result.getOrderTotalNum())
+                            + (dealerAggregatedDTO.getOrderTotalNum() == null ? 0 : dealerAggregatedDTO.getOrderTotalNum()));
+                    result.setTodayOrderNum((result.getTodayOrderNum() == null ? 0 : result.getTodayOrderNum())
+                            + (dealerAggregatedDTO.getTodayOrderNum() == null ? 0 : dealerAggregatedDTO.getTodayOrderNum()));
+                    result.setRecvTodayNum((result.getRecvTodayNum() == null ? 0 : result.getRecvTodayNum())
+                            + (dealerAggregatedDTO.getRecvTodayNum() == null ? 0 : dealerAggregatedDTO.getRecvTodayNum()));
+                    result.setRecvTotalNum((result.getRecvTotalNum() == null ? 0 : result.getRecvTotalNum())
+                            + (dealerAggregatedDTO.getRecvTotalNum() == null ? 0 : dealerAggregatedDTO.getRecvTotalNum()));
+                    result.setGoodsTotalNum((result.getGoodsTotalNum() == null ? 0 : result.getGoodsTotalNum())
+                            + (dealerAggregatedDTO.getGoodsTotalNum() == null ? 0 : dealerAggregatedDTO.getGoodsTotalNum()));
+                    result.setTodayGoodsNum((result.getTodayGoodsNum() == null ? 0 : result.getTodayGoodsNum())
+                            + (dealerAggregatedDTO.getTodayGoodsNum() == null ? 0 : dealerAggregatedDTO.getTodayGoodsNum()));
+                    result.setPadUsedNum((result.getPadUsedNum() == null ? 0 : result.getPadUsedNum())
+                            + (dealerAggregatedDTO.getPadUsedNum() == null ? 0 : dealerAggregatedDTO.getPadUsedNum()));
+                }
+            }
+        }
 
-        return R.ok().put("data",dealerAggregatedDTO);
+        return R.ok().put("data",result);
     }
 
+
     /**
      * 短信余额
      */
     @GetMapping("/smsBalance")
-    public R smsBalance(){
-        Long smsBalance = redisCache.getCacheObject(StatisticsRedisConstant.DATA_OVERVIEW_DEALER_SMS_BALANCE);
-
-        return R.ok().put("data", smsBalance);
+    public R smsBalance(StatisticsDeptCompanyParam param){
+        if (!medicalMallConfig.isStatics() || (param.getCompanyId() == null && param.getDeptId() == null) || (param.getCompanyId() == null && param.getDeptId() == 1)){
+            return R.ok().put("data", redisCache.getCacheObject(StatisticsRedisConstant.DATA_OVERVIEW_DEALER_SMS_BALANCE));
+        }else if(param.getCompanyId() != null){
+            return R.ok().put("data", redisCache.getCacheObject(String.format("%s:%d", DATA_OVERVIEW_DEALER_SMS_BALANCE, param.getCompanyId())));
+        }else{
+            Company company = new Company();
+            company.setDeptId(param.getDeptId());
+            long smsBalance = 0L;
+            Long[] companyIds = companyService.selectCompanyList(company).stream().map(Company::getCompanyId).toArray(Long[]::new);
+            for(Long companyId : companyIds) {
+                Long smsBalanceCompany = redisCache.getCacheObject(String.format("%s:%d", DATA_OVERVIEW_DEALER_SMS_BALANCE, companyId));
+                if (smsBalanceCompany != null) {
+                    smsBalance += smsBalanceCompany;
+                }
+            }
+            return R.ok().put("data", smsBalance);
+        }
     }
 
 
@@ -178,8 +699,33 @@ public class IndexStatisticsController {
      * 授权信息
      */
     @GetMapping("/authorizationInfo")
-    public R authorizationInfo(){
-        AuthorizationInfoDTO authorizationInfoDTO = redisCache.getCacheObject(StatisticsRedisConstant.DATA_OVERVIEW_DEALER_AUTHORIZATION_INFO);
+    public R authorizationInfo(StatisticsDeptCompanyParam  param){
+        AuthorizationInfoDTO authorizationInfoDTO = new AuthorizationInfoDTO();
+        if (!medicalMallConfig.isStatics() || (param.getCompanyId() == null && param.getDeptId() == null) || (param.getCompanyId() == null && param.getDeptId() == 1)){
+            return R.ok().put("data", redisCache.getCacheObject(StatisticsRedisConstant.DATA_OVERVIEW_DEALER_AUTHORIZATION_INFO));
+        }else if(param.getCompanyId() != null){
+            return R.ok().put("data", redisCache.getCacheObject(String.format("%s:%d", DATA_OVERVIEW_DEALER_AUTHORIZATION_INFO, param.getCompanyId())));
+        }else{
+            Company company = new Company();
+            company.setDeptId(param.getDeptId());
+            AuthorizationInfoDTO authorizationInfoDTO1 = new AuthorizationInfoDTO();
+            Long[] companyIds = companyService.selectCompanyList(company).stream().map(Company::getCompanyId).toArray(Long[]::new);
+            for(Long companyId : companyIds) {
+                AuthorizationInfoDTO companyDTO = redisCache.getCacheObject(String.format("%s:%d", DATA_OVERVIEW_DEALER_AUTHORIZATION_INFO, companyId));
+                log.info("授权信息:{}", authorizationInfoDTO);
+                if (companyDTO != null) {
+                    authorizationInfoDTO1.setTodayWatchUserCount(
+                            (authorizationInfoDTO1.getTodayWatchUserCount() == null ? 0 : authorizationInfoDTO1.getTodayWatchUserCount()) +
+                                    (companyDTO.getTodayWatchUserCount() == null ? 0 : companyDTO.getTodayWatchUserCount())
+                    );
+
+                    authorizationInfoDTO1.setVersionLimit(
+                            (authorizationInfoDTO1.getVersionLimit() == null ? 0 : authorizationInfoDTO1.getVersionLimit()) +
+                                    (companyDTO.getVersionLimit() == null ? 0 : companyDTO.getVersionLimit())
+                    );
+                }
+            }
+        }
 
         return R.ok().put("data", authorizationInfoDTO);
     }
@@ -190,9 +736,29 @@ public class IndexStatisticsController {
      * @return
      */
     @GetMapping("/thisMonthOrderCount")
-    public R thisMonthOrderCount(){
-        R result = redisCache.getCacheObject(StatisticsRedisConstant.THIS_MONTH_ORDER_COUNT);
-        return result;
+    public R thisMonthOrderCount(StatisticsDeptCompanyParam  param){
+        if (!medicalMallConfig.isStatics() || (param.getCompanyId() == null && param.getDeptId() == null) || (param.getCompanyId() == null && param.getDeptId() == 1)){
+            return redisCache.getCacheObject(StatisticsRedisConstant.THIS_MONTH_ORDER_COUNT);
+        }else if(param.getCompanyId() != null){
+            return redisCache.getCacheObject(String.format("%s:%d", THIS_MONTH_ORDER_COUNT, param.getCompanyId()));
+        }else{
+            Company company = new Company();
+            company.setDeptId(param.getDeptId());
+            Long[] companyIds = companyService.selectCompanyList(company).stream().map(Company::getCompanyId).toArray(Long[]::new);
+            List<String> datesList = new ArrayList<>();
+            List<Integer> orderCountList = new ArrayList<>();
+            List<Integer> payPriceList = new ArrayList<>();
+            for(Long companyId : companyIds) {
+                R result = redisCache.getCacheObject(String.format("%s:%d", THIS_MONTH_ORDER_COUNT, companyId));
+                Object datas = result.get("datas");
+                Object orderCount = result.get("orderCount");
+                Object payPrice = result.get("payPrice");
+                if(datas != null){
+                    mergeDataListsForInteger(datesList, orderCountList, payPriceList, (List<String>)datas, (List<Integer>)orderCount, (List<Integer>)payPrice);
+                }
+            }
+            return R.ok().put("dates", datesList).put("orderCount", orderCountList).put("payPrice", payPriceList);
+        }
     }
 
     /**
@@ -201,8 +767,112 @@ public class IndexStatisticsController {
      */
 
     @GetMapping("/thisMonthRecvCount")
-    public R thisMonthRecvCount(){
-        R result = redisCache.getCacheObject(StatisticsRedisConstant.THIS_MONTH_RECV_COUNT);
-        return result;
+    public R thisMonthRecvCount(StatisticsDeptCompanyParam  param){
+        if (!medicalMallConfig.isStatics() || (param.getCompanyId() == null && param.getDeptId() == null) || (param.getCompanyId() == null && param.getDeptId() == 1)){
+            return redisCache.getCacheObject(StatisticsRedisConstant.THIS_MONTH_RECV_COUNT);
+        }else if(param.getCompanyId() != null){
+            return redisCache.getCacheObject(String.format("%s:%d", THIS_MONTH_RECV_COUNT, param.getCompanyId()));
+        }else{
+            Company company = new Company();
+            company.setDeptId(param.getDeptId());
+            Long[] companyIds = companyService.selectCompanyList(company).stream().map(Company::getCompanyId).toArray(Long[]::new);
+            List<String> datesList = new ArrayList<>();
+            List<Integer> orderCountList = new ArrayList<>();
+            List<Float> payMoneyList = new ArrayList<>();
+            for(Long companyId : companyIds) {
+               R result = redisCache.getCacheObject(String.format("%s:%d", THIS_MONTH_RECV_COUNT, companyId));
+               Object datas = result.get("datas");
+               Object orderCount = result.get("orderCount");
+               Object payMoney = result.get("payMoney");
+               if(datas != null){
+                   mergeDataLists(datesList, orderCountList, payMoneyList, (List<String>)datas, (List<Integer>)orderCount, (List<Float>)payMoney);
+               }
+            }
+            return R.ok().put("dates", datesList).put("orderCount", orderCountList).put("payMoney", payMoneyList);
+        }
+    }
+
+    private void mergeDataLists(List<String> datasList, List<Integer> orderCountList, List<Float> payMoneyList,
+                               List<String> datas, List<Integer> orderCount, List<Float> payMoney) {
+
+        // 遍历新数据
+        for (int i = 0; i < datas.size(); i++) {
+            String newData = datas.get(i);
+            Integer newOrderCount = orderCount.get(i);
+            Float newPayMoney = payMoney.get(i);
+
+            // 查找在现有列表中的位置
+            int existingIndex = datasList.indexOf(newData);
+
+            if (existingIndex != -1) {
+                // 如果存在,将orderCount和payMoney相加
+                Integer existingOrderCount = orderCountList.get(existingIndex);
+                Float existingPayMoney = payMoneyList.get(existingIndex);
+
+                // 累加orderCount
+                orderCountList.set(existingIndex,
+                        (existingOrderCount == null ? 0 : existingOrderCount) +
+                                (newOrderCount == null ? 0 : newOrderCount));
+
+                // 累加payMoney
+                payMoneyList.set(existingIndex,
+                        (existingPayMoney == null ? 0.0f : existingPayMoney) +
+                                (newPayMoney == null ? 0.0f : newPayMoney));
+            } else {
+                // 如果不存在,直接添加新项
+                datasList.add(newData);
+                orderCountList.add(newOrderCount);
+                payMoneyList.add(newPayMoney);
+            }
+        }
+    }
+
+    private void mergeDataListsForInteger(List<String> datasList, List<Integer> orderCountList, List<Integer> payMoneyList,
+                                          List<String> datas, List<Integer> orderCount, List<Integer> payMoney) {
+        // 遍历新数据
+        for (int i = 0; i < datas.size(); i++) {
+            String newData = datas.get(i);
+            Integer newOrderCount = orderCount.get(i);
+            Integer newPayMoney = payMoney.get(i);
+
+            // 查找在现有列表中的位置
+            int existingIndex = datasList.indexOf(newData);
+
+            if (existingIndex != -1) {
+                // 如果存在,将orderCount和payMoney相加
+                Integer existingOrderCount = orderCountList.get(existingIndex);
+                Integer existingPayMoney = payMoneyList.get(existingIndex);
+
+                // 累加orderCount
+                orderCountList.set(existingIndex,
+                        (existingOrderCount == null ? 0 : existingOrderCount) +
+                                (newOrderCount == null ? 0 : newOrderCount));
+
+                // 累加payMoney
+                payMoneyList.set(existingIndex,
+                        (existingPayMoney == null ? 0 : existingPayMoney) +
+                                (newPayMoney == null ? 0 : newPayMoney));
+            } else {
+                // 如果不存在,直接添加新项
+                datasList.add(newData);
+                orderCountList.add(newOrderCount);
+                payMoneyList.add(newPayMoney);
+            }
+        }
+    }
+
+    /**
+     * @Description: 看课统计按公司
+     * @Param:
+     * @Return:
+     * @Author xgb
+     * @Date 2025/10/27 16:29
+     */
+    @PostMapping("/getWatchCourseStatisticsData")
+    public R getWatchCourseStatisticsData(@RequestBody AnalysisPreviewQueryDTO param){
+        // 从缓存获取看客统计数据
+        List<WatchCourseStatisticsResultDTO> data=statisticsService.getWatchCourseStatisticsData( param);
+
+        return R.ok().put("data", data);
     }
 }

+ 78 - 0
fs-admin/src/main/java/com/fs/api/controller/StatisticManageController.java

@@ -0,0 +1,78 @@
+package com.fs.api.controller;
+
+import com.fs.common.core.domain.R;
+import com.fs.company.service.IStatisticManageService;
+import com.fs.statis.param.ComprehensiveStatisticsParam;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.util.Assert;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+
+/**
+ * @description: 统计管理
+ * @author: Guos
+ * @time: 2025/10/30 上午9:16
+ */
+@Slf4j
+@RestController
+@RequestMapping("/statistic/manage")
+public class StatisticManageController {
+
+    @Resource
+    private IStatisticManageService statisticManageService;
+
+    /**
+     * 日综合获取统计信息(需要按时间分组)
+     * @param param
+     * @return
+     */
+    @PostMapping("/statisticMain")
+    public R statisticMain(@RequestBody ComprehensiveStatisticsParam param) {
+        Assert.notNull(param.getDimension(), "请选择统计维度");
+        param.setTimeGroupFlag(true);
+        return R.ok().put("data", statisticManageService.statisticMain(param));
+    }
+
+    /**
+     * 获取综合统计信息(不需要按时间分组)
+     * @param param
+     * @return
+     */
+    @PostMapping("/statisticMainN")
+    public R statisticMainN(@RequestBody ComprehensiveStatisticsParam param) {
+        Assert.notNull(param.getDimension(), "请选择统计维度");
+        param.setTimeGroupFlag(false);
+        return R.ok().put("data", statisticManageService.statisticMain(param));
+    }
+
+    /**
+     * 获取公司下拉
+     * @return
+     */
+    @GetMapping("/getSearchCompanyInfo")
+    public R getSearchCompanyInfo(){
+        return R.ok().put("data", statisticManageService.getSearchCompanyInfo());
+    }
+
+    /**
+     * 根据公司id获取部门下拉
+     * @return
+     */
+    @GetMapping("/getSearchDeptInfo")
+    public R getSearchDeptInfo(@RequestParam("id") Long id){
+        return R.ok().put("data", statisticManageService.getSearchDeptInfo(id));
+    }
+
+    /**
+     * 根据部门id获取用户下拉信息
+     * @return
+     */
+    @GetMapping("/getSearchUserInfo")
+    public R getSearchUserInfo(@RequestParam("id") Long id){
+        return R.ok().put("data", statisticManageService.getSearchUserInfo(id));
+    }
+
+
+
+}

+ 48 - 0
fs-admin/src/main/java/com/fs/company/controller/CompanyConfigController.java

@@ -0,0 +1,48 @@
+package com.fs.company.controller;
+
+
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.company.service.ICompanyConfigService;
+import com.fs.system.domain.SysConfig;
+import com.fs.system.service.ISysConfigService;
+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;
+
+/**
+ * 系统配置
+ *
+
+ */
+@RestController
+@RequestMapping("/company/companyConfig")
+public class CompanyConfigController extends BaseController
+{
+
+    @Autowired
+    private ICompanyConfigService companyConfigService;
+
+
+    @Autowired
+    private ISysConfigService configService;
+
+
+
+    /**
+     * 根据参数键名查询总后台参数值
+     * @param configKey
+     * @return
+     */
+    @GetMapping(value = "/getConfigByKey/{configKey}")
+    public AjaxResult getConfigByKey(@PathVariable String configKey)
+    {
+        SysConfig config=configService.selectConfigByConfigKey(configKey);
+        return AjaxResult.success(config);
+    }
+
+
+
+}

+ 68 - 5
fs-admin/src/main/java/com/fs/company/controller/CompanyController.java

@@ -1,6 +1,7 @@
 package com.fs.company.controller;
 
 import cn.hutool.core.util.IdUtil;
+import cn.hutool.json.JSONUtil;
 import com.fs.common.annotation.Log;
 import com.fs.common.annotation.RepeatSubmit;
 import com.fs.common.core.controller.BaseController;
@@ -9,28 +10,29 @@ import com.fs.common.core.domain.R;
 import com.fs.common.core.domain.model.LoginUser;
 import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.ParseUtils;
 import com.fs.common.utils.SecurityUtils;
 import com.fs.common.utils.ServletUtils;
 import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.common.utils.sign.Md5Utils;
 import com.fs.company.domain.*;
-import com.fs.company.param.CompanyDeductParam;
-import com.fs.company.param.CompanyParam;
-import com.fs.company.param.CompanyRechargeParam;
-import com.fs.company.param.CompanyVoiceCallerParam;
+import com.fs.company.param.*;
 import com.fs.company.service.*;
 import com.fs.company.vo.CompanyCrmVO;
 import com.fs.company.vo.CompanyVO;
 import com.fs.company.vo.CompanyVoiceCallerListVO;
 import com.fs.core.utils.OrderCodeUtils;
+import com.fs.course.config.CourseConfig;
 import com.fs.framework.web.service.TokenService;
 import com.fs.his.vo.OptionsVO;
+import com.fs.system.service.ISysConfigService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.web.bind.annotation.*;
 
+import java.text.ParseException;
 import java.util.List;
 
 /**
@@ -55,6 +57,32 @@ public class CompanyController extends BaseController
     private ICompanyDeductService deductService;
     @Autowired
     private ICompanyVoiceCallerService callerService;
+    @Autowired
+    private ISysConfigService configService;
+
+    /**
+     * 查询企业列表
+     */
+    @PreAuthorize("@ss.hasPermi('live:liveMiniLives:list')")
+    @GetMapping("/liveShowList")
+    public TableDataInfo liveShowList(CompanyParam param) throws ParseException {
+        startPage();
+        List<CompanyVO> list = companyService.liveShowList(param);
+        for (CompanyVO vo : list){
+            vo.setCompanyMobile(ParseUtils.parsePhone(vo.getCompanyMobile()));
+        }
+        return getDataTable(list);
+    }
+
+    @PostMapping(value = "/batchUpdateLiveShow")
+    public R batchUpdateLiveShow(@RequestBody CompanyLiveShowParam param) {
+        if (param.getIds().isEmpty()) {
+            return R.error("请选择要操作的记录");
+        }
+        companyService.batchUpdateLiveShow(param);
+        return R.ok();
+    }
+
     /**
      * 查询企业列表
      */
@@ -63,6 +91,12 @@ public class CompanyController extends BaseController
     public TableDataInfo list(CompanyParam param)
     {
         startPage();
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        String json = configService.selectConfigByKey("course.config");
+        CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
+        if(!loginUser.isAdmin() && config.getDept() != null && config.getDept()){
+            param.setDeptId(loginUser.getDeptId());
+        }
         List<CompanyVO> list = companyService.selectCompanyVOList(param);
         return getDataTable(list);
     }
@@ -75,6 +109,12 @@ public class CompanyController extends BaseController
     @GetMapping("/export")
     public AjaxResult export(CompanyParam company)
     {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        String json = configService.selectConfigByKey("course.config");
+        CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
+        if(!loginUser.isAdmin() && config.getDept() != null && config.getDept()){
+            company.setDeptId(loginUser.getDeptId());
+        }
         List<CompanyVO> list = companyService.selectCompanyVOList(company);
         ExcelUtil<CompanyVO> util = new ExcelUtil<CompanyVO>(CompanyVO.class);
         return util.exportExcel(list, "company");
@@ -98,6 +138,10 @@ public class CompanyController extends BaseController
     @PostMapping
     public R add(@RequestBody Company company)
     {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        if(company.getDeptId() != null){
+            company.setDeptId(loginUser.getDeptId());
+        }
         company.setPassword(SecurityUtils.encryptPassword(company.getPassword()));
         company.setAppId(Md5Utils.hash(company.getUserName()));
         company.setAppKey(Md5Utils.hash(company.getPassword()));
@@ -154,6 +198,12 @@ public class CompanyController extends BaseController
     {
         Company map=new Company();
         map.setIsDel(0);
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        String json = configService.selectConfigByKey("course.config");
+        CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
+        if(!loginUser.isAdmin() && config.getDept() != null && config.getDept()){
+            map.setDeptId(loginUser.getDeptId());
+        }
         List<Company> list = companyService.selectCompanyList(map);
         return R.ok().put("data",list);
     }
@@ -164,6 +214,12 @@ public class CompanyController extends BaseController
     public TableDataInfo companyCrmDayCountList(CompanyParam param)
     {
         startPage();
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        String json = configService.selectConfigByKey("course.config");
+        CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
+        if(!loginUser.isAdmin() && config.getDept() != null && config.getDept()){
+            param.setDeptId(loginUser.getDeptId());
+        }
         List<CompanyCrmVO> list = companyService.selectCompanyCrmDayCountList(param);
         return getDataTable(list);
     }
@@ -239,7 +295,14 @@ public class CompanyController extends BaseController
     @GetMapping("/allList")
     public TableDataInfo getHospital()
     {
-        List<OptionsVO> list = companyService.selectAllCompanyList();
+        Long deptId = null;
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        String json = configService.selectConfigByKey("course.config");
+        CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
+        if(!loginUser.isAdmin() && config.getDept() != null && config.getDept()){
+            deptId = loginUser.getDeptId();
+        }
+        List<OptionsVO> list = companyService.selectAllCompanyList(deptId);
         return getDataTable(list);
     }
 }

+ 5 - 0
fs-admin/src/main/java/com/fs/company/controller/CompanyDeductController.java

@@ -14,6 +14,7 @@ import com.fs.company.domain.CompanyDeduct;
 import com.fs.company.domain.CompanyMoneyLogs;
 import com.fs.company.service.ICompanyDeductService;
 import com.fs.company.service.ICompanyMoneyLogsService;
+import com.fs.company.service.ICompanyRechargeService;
 import com.fs.company.service.ICompanyService;
 import com.fs.company.vo.CompanyDeductExportVO;
 import com.fs.company.vo.CompanyDeductVO;
@@ -45,6 +46,10 @@ public class CompanyDeductController extends BaseController
     private ICompanyService companyService;
     @Autowired
     private ICompanyMoneyLogsService moneyLogsService;
+
+    @Autowired
+    private ICompanyRechargeService companyRechargeService;
+
     /**
      * 查询扣款列表
      */

+ 1 - 0
fs-admin/src/main/java/com/fs/company/controller/CompanyRechargeController.java

@@ -24,6 +24,7 @@ import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.web.bind.annotation.*;
 
+import java.math.BigDecimal;
 import java.util.Date;
 import java.util.List;
 

+ 103 - 0
fs-admin/src/main/java/com/fs/company/controller/CompanyRedPacketBalanceLogsController.java

@@ -0,0 +1,103 @@
+package com.fs.company.controller;
+
+import java.util.List;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.enums.BusinessType;
+import com.fs.company.domain.CompanyRedPacketBalanceLogs;
+import com.fs.company.service.ICompanyRedPacketBalanceLogsService;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.common.core.page.TableDataInfo;
+
+/**
+ * 企业红包余额记录Controller
+ * 
+ * @author fs
+ * @date 2025-11-19
+ */
+@RestController
+@RequestMapping("/company/companyRedPacketBalanceLogs")
+public class CompanyRedPacketBalanceLogsController extends BaseController
+{
+    @Autowired
+    private ICompanyRedPacketBalanceLogsService companyRedPacketBalanceLogsService;
+
+    /**
+     * 查询企业红包余额记录列表
+     */
+    @PreAuthorize("@ss.hasPermi('company:companyRedPacketBalanceLogs:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(CompanyRedPacketBalanceLogs companyRedPacketBalanceLogs)
+    {
+        startPage();
+        List<CompanyRedPacketBalanceLogs> list = companyRedPacketBalanceLogsService.selectCompanyRedPacketBalanceLogsList(companyRedPacketBalanceLogs);
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出企业红包余额记录列表
+     */
+    @PreAuthorize("@ss.hasPermi('company:companyRedPacketBalanceLogs:export')")
+    @Log(title = "企业红包余额记录", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(CompanyRedPacketBalanceLogs companyRedPacketBalanceLogs)
+    {
+        List<CompanyRedPacketBalanceLogs> list = companyRedPacketBalanceLogsService.selectCompanyRedPacketBalanceLogsList(companyRedPacketBalanceLogs);
+        ExcelUtil<CompanyRedPacketBalanceLogs> util = new ExcelUtil<CompanyRedPacketBalanceLogs>(CompanyRedPacketBalanceLogs.class);
+        return util.exportExcel(list, "企业红包余额记录数据");
+    }
+
+    /**
+     * 获取企业红包余额记录详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('company:companyRedPacketBalanceLogs:query')")
+    @GetMapping(value = "/{logsId}")
+    public AjaxResult getInfo(@PathVariable("logsId") Long logsId)
+    {
+        return AjaxResult.success(companyRedPacketBalanceLogsService.selectCompanyRedPacketBalanceLogsByLogsId(logsId));
+    }
+
+    /**
+     * 新增企业红包余额记录
+     */
+    @PreAuthorize("@ss.hasPermi('company:companyRedPacketBalanceLogs:add')")
+    @Log(title = "企业红包余额记录", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody CompanyRedPacketBalanceLogs companyRedPacketBalanceLogs)
+    {
+        return toAjax(companyRedPacketBalanceLogsService.insertCompanyRedPacketBalanceLogs(companyRedPacketBalanceLogs));
+    }
+
+    /**
+     * 修改企业红包余额记录
+     */
+    @PreAuthorize("@ss.hasPermi('company:companyRedPacketBalanceLogs:edit')")
+    @Log(title = "企业红包余额记录", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody CompanyRedPacketBalanceLogs companyRedPacketBalanceLogs)
+    {
+        return toAjax(companyRedPacketBalanceLogsService.updateCompanyRedPacketBalanceLogs(companyRedPacketBalanceLogs));
+    }
+
+    /**
+     * 删除企业红包余额记录
+     */
+    @PreAuthorize("@ss.hasPermi('company:companyRedPacketBalanceLogs:remove')")
+    @Log(title = "企业红包余额记录", businessType = BusinessType.DELETE)
+	@DeleteMapping("/{logsIds}")
+    public AjaxResult remove(@PathVariable Long[] logsIds)
+    {
+        return toAjax(companyRedPacketBalanceLogsService.deleteCompanyRedPacketBalanceLogsByLogsIds(logsIds));
+    }
+}

+ 108 - 0
fs-admin/src/main/java/com/fs/company/controller/CompanyStatisticsController.java

@@ -19,8 +19,14 @@ import com.fs.crm.service.ICrmCustomerService;
 import com.fs.crm.service.ICrmCustomerVisitService;
 import com.fs.crm.vo.CrmCustomerStatisticsVO;
 import com.fs.crm.vo.CrmCustomerVisitStatisticsVO;
+import com.fs.his.dto.FsStoreOrderAmountScrmStatsQueryDto;
+import com.fs.his.dto.FsStoreOrderAmountStatsQueryDto;
+import com.fs.his.service.IFsStoreAfterSalesService;
 import com.fs.his.service.IFsStoreOrderService;
 import com.fs.his.service.IFsStorePaymentService;
+import com.fs.his.vo.FsStoreOrderAmountScrmStatsVo;
+import com.fs.his.vo.FsStoreOrderAmountStatsVo;
+import com.fs.hisStore.service.IFsStoreOrderScrmService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.GetMapping;
@@ -31,6 +37,7 @@ import org.springframework.web.bind.annotation.RestController;
 import java.math.BigDecimal;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Map;
 import java.util.stream.Collectors;
 
 /**
@@ -49,6 +56,10 @@ public class CompanyStatisticsController extends BaseController
 
     @Autowired
     private IFsStoreOrderService storeOrderService;
+
+    @Autowired
+    private IFsStoreAfterSalesService storeAfterSalesService;
+
     @Autowired
     private IFsStorePaymentService storePaymentService;
     @Autowired
@@ -61,6 +72,10 @@ public class CompanyStatisticsController extends BaseController
     private ICrmCustomerService crmCustomerService;
     @Autowired
     private ICrmCustomerVisitService crmCustomerVisitService;
+
+    //app商城订单接口Service
+    @Autowired
+    private IFsStoreOrderScrmService fsStoreOrderScrmService;
     @GetMapping("/storeOrder")
     public R storeOrder(FsStoreStatisticsParam param)
     {
@@ -146,6 +161,55 @@ public class CompanyStatisticsController extends BaseController
         }
     }
 
+    @GetMapping("/afterSalesOrder")
+    public R afterSalesOrder(FsStoreStatisticsParam param)
+    {
+        if(StringUtils.isNotEmpty(param.getUserIds())){
+            String[] userIds=param.getUserIds().split(",");
+            Long[] ids=new Long[userIds.length];
+            for(int i=0;i<ids.length; i++){
+                ids[i]=Long.parseLong(userIds[i]);
+            }
+            param.setUsers(ids);
+        }
+        else{
+            //获取部门下的所有用户
+            CompanyUser usermap=new CompanyUser();
+            usermap.setDeptId(param.getDeptId());
+            List<CompanyUser> users = userService.getUserListByDeptId(usermap);
+            List<Long> userIds = users.stream().map(element -> element.getUserId()).collect(Collectors.toList());
+            param.setUsers(userIds.toArray(new Long[userIds.size()]));
+        }
+        if(param.getUsers()!=null&&param.getUsers().length>0){
+            List<FsStoreOrderStatisticsVO> list= storeAfterSalesService.selectFsStoreAfterSalesServiceStatisticsList(param);
+
+            TimeUtils.TimeEntity timeEntity= TimeUtils.parseTime(param.getType().toString(),param.getStartTime(),param.getEndTime());
+            timeEntity.setUserIds(param.getUsers());
+            Integer cycleNum = timeEntity.getCycleNum();
+            Integer beginTime = timeEntity.getBeginTime();
+            List<Integer> timeList = new ArrayList<>();
+            for (int i = 1; i <= cycleNum; i++) {
+                timeList.add(beginTime);
+                beginTime = TimeUtils.formatTime(beginTime);
+            }
+            Map<String, Object> map = timeEntity.toMap();
+            if (StringUtils.isNotBlank(param.getStartTime())){
+                map.put("startTime",param.getStartTime());
+            }
+            if (StringUtils.isNotBlank(param.getEndTime())){
+                map.put("endTime",param.getEndTime());
+            }
+            List<JSONObject> jsonObjectList = storeAfterSalesService.selectFsStoreAfterSales(map);
+            List<String> dates = jsonObjectList.stream().map(jsonObject -> jsonObject.getString("type")).collect(Collectors.toList());
+            List<Integer> orderCount = jsonObjectList.stream().map(jsonObject -> jsonObject.getInteger("orderCount")).collect(Collectors.toList());
+            List<BigDecimal> payPrice = jsonObjectList.stream().map(jsonObject -> jsonObject.getBigDecimal("payPrice")).collect(Collectors.toList());
+            return R.ok().put("list",list).put("dates",dates).put("orderCount",orderCount).put("payPrice",payPrice);
+        }
+        else {
+            return R.ok("未查找到数据");
+        }
+    }
+
 
     @GetMapping("/inquiryOrder")
     public R storeInquiryOrder(FsStoreStatisticsParam param)
@@ -244,6 +308,32 @@ public class CompanyStatisticsController extends BaseController
         return util.exportExcel(list, "orderLogs");
     }
 
+    @GetMapping("/exportAfterSalesOrder")
+    public AjaxResult exportAfterSalesOrder(FsStoreStatisticsParam param)
+    {
+        if(StringUtils.isNotEmpty(param.getUserIds())){
+            String[] userIds=param.getUserIds().split(",");
+            Long[] ids=new Long[userIds.length];
+            for(int i=0;i<ids.length; i++){
+                ids[i]=Long.parseLong(userIds[i]);
+            }
+            param.setUsers(ids);
+        }
+        else{
+            //获取所有员工
+            CompanyUser usermap=new CompanyUser();
+            usermap.setDeptId(param.getDeptId());
+            List<CompanyUser> users = userService.getUserListByDeptId(usermap);
+            List<Long> userIds = users.stream().map(element -> element.getUserId()).collect(Collectors.toList());
+            param.setUsers(userIds.toArray(new Long[userIds.size()]));
+        }
+
+        List<FsStoreOrderStatisticsVO> list= storeAfterSalesService.selectFsStoreAfterSalesServiceStatisticsList(param);
+
+        ExcelUtil<FsStoreOrderStatisticsVO> util = new ExcelUtil<FsStoreOrderStatisticsVO>(FsStoreOrderStatisticsVO.class);
+        return util.exportExcel(list, "orderLogs");
+    }
+
 
     @GetMapping("/exportInquiryOrder")
     public AjaxResult exportInquiryOrder(FsStoreStatisticsParam param)
@@ -643,4 +733,22 @@ public class CompanyStatisticsController extends BaseController
         return util.exportExcel(qwIpadTotalVos, "visit");
     }
 
+    /**
+     * 获取互联网医院订单统计数据
+     * */
+    @GetMapping("/hisOrderCountStats")
+    public AjaxResult getHisOrderCount(FsStoreOrderAmountStatsQueryDto statsQueryDto){
+        FsStoreOrderAmountStatsVo fsStoreOrderAmountStatsVo = storeOrderService.selectFsStoreOrderAmountStats(statsQueryDto);
+        return AjaxResult.success(fsStoreOrderAmountStatsVo);
+    }
+
+    /**
+     * 获取App商城订单统计数据
+     * */
+    @GetMapping("/appOrderCountStats")
+    public AjaxResult getAppOrderCount(FsStoreOrderAmountScrmStatsQueryDto statsQueryDto){
+        FsStoreOrderAmountScrmStatsVo scrmStatsVo = fsStoreOrderScrmService.selectFsStoreOrderAmountScrmStats(statsQueryDto);
+        return AjaxResult.success(scrmStatsVo);
+    }
+
 }

+ 103 - 0
fs-admin/src/main/java/com/fs/company/controller/CompanyTrafficController.java

@@ -0,0 +1,103 @@
+package com.fs.company.controller;
+
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.R;
+import com.fs.common.core.domain.model.LoginUser;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.ServletUtils;
+import com.fs.company.domain.CompanyTrafficRecord;
+import com.fs.company.param.CompanyTrafficRecordChargeParam;
+import com.fs.company.param.CompanyTrafficRecordQueryParam;
+import com.fs.company.service.ICompanyTrafficRecordService;
+import com.fs.framework.web.service.TokenService;
+import com.fs.hisStore.config.MedicalMallConfig;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.PostConstruct;
+import java.util.List;
+
+@RestController
+@RequestMapping("/company/traffic")
+public class CompanyTrafficController extends BaseController {
+
+    @Autowired
+    private ICompanyTrafficRecordService companyTrafficRecordService;
+
+    @Autowired
+    private TokenService tokenService;
+
+    @Autowired
+    private MedicalMallConfig medicalMallConfig;
+
+    /**
+     * 每天扣除流量余额
+     * */
+    @Scheduled(cron = "0 1 0 * * ?")
+    public void refreshTraffic(){
+        if(medicalMallConfig.isStatics())
+            companyTrafficRecordService.refreshTraffic();
+    }
+
+    /**
+     * 重启时重新计算并更新公司缓存
+     * */
+    @PostConstruct
+    public void init() {
+        if(medicalMallConfig.isStatics())
+            companyTrafficRecordService.init();
+    }
+
+    /** 充值流量 */
+    @PreAuthorize("@ss.hasPermi('company:traffic:charge')")
+    @Log(title = "公司流量充值", businessType = BusinessType.INSERT)
+    @PostMapping(value = "/rechargeTraffic")
+    public R rechargeTraffic(@RequestBody CompanyTrafficRecordChargeParam companyTrafficRecord) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        companyTrafficRecord.setUserId(loginUser.getUser().getUserId());
+        companyTrafficRecord.setUserName(loginUser.getUser().getUserName());
+        companyTrafficRecordService.recharge(companyTrafficRecord);
+        return R.ok();
+    }
+
+    /** 流量充值记录查询 */
+    @PreAuthorize("@ss.hasPermi('company:traffic:list')")
+    @GetMapping(value = "/list")
+    public TableDataInfo list(CompanyTrafficRecordQueryParam param) {
+        startPage();
+        List<CompanyTrafficRecord> list = companyTrafficRecordService.selectList(param);
+        return getDataTable(list);
+    }
+
+    /** 流量充值详情 */
+    @PreAuthorize("@ss.hasPermi('company:traffic:detail')")
+    @GetMapping(value = "/detail/{logId}")
+    public R detail(@PathVariable("logId") Long logId) {
+        CompanyTrafficRecord record = companyTrafficRecordService.selectById(logId);
+        return R.ok().put("data", record);
+    }
+
+    /** 流量换算*/
+    @GetMapping(value = "/trafficConversion")
+    public R trafficConversion(@RequestParam("traffic") Long amount) {
+        Long trafficKB = companyTrafficRecordService.trafficConversion(amount);
+        return R.ok().put("data", trafficKB);
+    }
+
+    /** 刷新单价*/
+    @GetMapping(value = "/refreshPrice")
+    public R refreshPrice() {
+        companyTrafficRecordService.refreshPrice();
+        return R.ok();
+    }
+
+    /** 流量统计*/
+    @GetMapping(value = "/trafficStatistics")
+    public R trafficStatistics(CompanyTrafficRecord record) {
+        return R.ok().put("tatol","").put("dept","").put("company","");
+    }
+}

+ 49 - 0
fs-admin/src/main/java/com/fs/company/controller/CompanyTrafficLogController.java

@@ -0,0 +1,49 @@
+package com.fs.company.controller;
+
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.domain.R;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.company.domain.CompanyTrafficRecordLog;
+import com.fs.company.param.CompanyTrafficRecordLogQueryParam;
+import com.fs.company.service.ICompanyTrafficRecordLogService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/company/trafficLog")
+public class CompanyTrafficLogController extends BaseController {
+
+    @Autowired
+    private ICompanyTrafficRecordLogService companyTrafficRecordLogService;
+
+
+    /** 流量充值记录查询 */
+    @PreAuthorize("@ss.hasPermi('company:trafficLog:list')")
+    @GetMapping(value = "/list")
+    public TableDataInfo list(CompanyTrafficRecordLogQueryParam param) {
+        startPage();
+        List<CompanyTrafficRecordLog> list = companyTrafficRecordLogService.selectList(param);
+        return getDataTable(list);
+    }
+
+
+    /** 导出 */
+    @PreAuthorize("@ss.hasPermi('company:trafficLog:export')")
+    @Log(title = "流量充值记录导出", businessType = BusinessType.EXPORT)
+    @GetMapping(value = "/export")
+    public AjaxResult export(CompanyTrafficRecordLogQueryParam param) {
+        List<CompanyTrafficRecordLog> list = companyTrafficRecordLogService.selectList(param);
+        ExcelUtil<CompanyTrafficRecordLog> util = new ExcelUtil<>(CompanyTrafficRecordLog.class);
+        return util.exportExcel(list, "流量充值记录");
+    }
+
+
+
+}

+ 572 - 0
fs-admin/src/main/java/com/fs/company/controller/CompanyUserAllController.java

@@ -0,0 +1,572 @@
+package com.fs.company.controller;
+
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.http.HttpRequest;
+import cn.hutool.json.JSONUtil;
+import com.baidu.dev2.thirdparty.jackson.databind.ObjectMapper;
+import com.fs.common.annotation.Log;
+import com.fs.common.constant.UserConstants;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.domain.R;
+import com.fs.common.core.domain.model.LoginUser;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.PatternUtils;
+import com.fs.common.utils.SecurityUtils;
+import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.StringUtils;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.company.domain.*;
+import com.fs.company.param.CompanyUserAreaParam;
+import com.fs.company.param.CompanyUserCodeParam;
+import com.fs.company.param.CompanyUserQwParam;
+import com.fs.company.service.*;
+import com.fs.company.vo.BatchUserRolesVO;
+import com.fs.company.vo.CompanyUserImportVO;
+import com.fs.company.vo.CompanyUserQwListVO;
+import com.fs.company.vo.CompanyUserVO;
+import com.fs.course.config.CourseConfig;
+import com.fs.framework.web.service.TokenService;
+import com.fs.his.utils.qrcode.QRCodeUtils;
+import com.fs.his.vo.OptionsVO;
+import com.fs.hisStore.vo.FsStoreProductExportVO;
+import com.fs.im.config.IMConfig;
+import com.fs.im.dto.OpenImResponseDTO;
+import com.fs.im.service.OpenIMService;
+import com.fs.qw.domain.QwCompany;
+import com.fs.qw.service.IQwCompanyService;
+import com.fs.qw.service.IQwUserService;
+import com.fs.qw.vo.CompanyUserQwVO;
+import com.fs.qw.vo.QwUserVO;
+import com.fs.system.service.ISysConfigService;
+import com.fs.utils.DomainUtil;
+import com.fs.utils.QwStatusEnum;
+import com.fs.voice.utils.StringUtil;
+import com.github.pagehelper.PageHelper;
+import com.github.pagehelper.PageInfo;
+import io.swagger.annotations.ApiOperation;
+import org.json.JSONArray;
+import org.json.JSONObject;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.util.Assert;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.*;
+import java.util.concurrent.CompletableFuture;
+import java.util.stream.Collectors;
+
+
+/**
+ * 用户信息
+ */
+@RestController
+@RequestMapping("/company/CompanyUserAll")
+public class CompanyUserAllController extends BaseController {
+
+    @Autowired
+    private ICompanyRoleService roleService;
+
+    @Autowired
+    private ICompanyPostService postService;
+
+    @Autowired
+    private ICompanyUserService companyUserService;
+
+    @Autowired
+    private ICompanyService companyService;
+
+    @Autowired
+    private ICompanyUserDelayTimeService companyUserDelayTimeService;
+
+    @Autowired
+    private ISysConfigService configService;
+
+    @Autowired
+    private RedisCache redisCache;
+
+    @Autowired
+    private OpenIMService openIMService;
+
+    @Autowired
+    IQwCompanyService iQwCompanyService;
+
+    @Autowired
+    private IQwUserService qwUserService;
+
+    @Autowired
+    private TokenService tokenService;
+
+    private static final String appLink = "https://jump.ylrztop.com/jumpapp/pages/index/index?link=";
+
+
+    @GetMapping("/getList")
+    public TableDataInfo getList(CompanyUser user)
+    {
+        startPage();
+        List<CompanyUser> list = companyUserService.selectCompanyUserList(user);
+        return getDataTable(list);
+    }
+    @GetMapping("/qwList")
+    public TableDataInfo qwList(CompanyUserQwParam user) {
+        List<CompanyUserQwListVO> list = companyUserService.selectCompanyUserQwListVO(user);
+        if (!list.isEmpty()){
+            String json = configService.selectConfigByKey("course.config");
+            CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
+            Long sendDelayTime = config.getSendDelayTime();
+            List<CompletableFuture<Void>> futures = new ArrayList<>();
+            for (CompanyUserQwListVO companyUserQwListVO : list) {
+                CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
+                    CompanyUserDelayTime companyUserDelayTime = companyUserDelayTimeService.selectCompanyUserDelayTimeByCompanyUser(companyUserQwListVO.getCompanyId(), companyUserQwListVO.getUserId());
+                    if (ObjectUtil.isEmpty(companyUserDelayTime)) {
+                        companyUserQwListVO.setSendDelayTime(sendDelayTime);
+                    } else {
+                        companyUserQwListVO.setSendDelayTime(companyUserDelayTime.getSendDelayTime());
+                    }
+                    //是否绑定
+                    if(QwStatusEnum.BOUND.getCode() == companyUserQwListVO.getQwStatus()){
+                        if(!companyUserQwListVO.getQwUserId().isEmpty()){
+                            Long[] ids = Arrays.stream(companyUserQwListVO.getQwUserId().split(","))
+                                    .map(Long::parseLong)
+                                    .toArray(Long[]::new);
+                            List<QwUserVO> qwUserVOS = qwUserService.selectQwUserVOByIds(ids);
+                            companyUserQwListVO.setQwUsers(qwUserVOS);
+                        }
+                    }
+                });
+                futures.add(future);
+            }
+            CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
+        }
+        return getDataTable(list);
+    }
+
+    @Log(title = "用户管理导出", businessType = BusinessType.EXPORT)
+    @PreAuthorize("@ss.hasPermi('company:user:export')")
+    @GetMapping("/export")
+    public AjaxResult export(CompanyUser user)
+    {
+        List<CompanyUser> list = companyUserService.selectCompanyUserList(user);
+        ExcelUtil<CompanyUser> util = new ExcelUtil<CompanyUser>(CompanyUser.class);
+        return util.exportExcel(list, "用户数据");
+    }
+
+
+    @Log(title = "销售信息导入", businessType = BusinessType.IMPORT,isStoreLog = true,logParam = {"销售","信息导入"})
+    @PreAuthorize("@ss.hasPermi('company:user:import')")
+    @PostMapping("/importCompanyUser")
+    public AjaxResult importData(@RequestParam("file") MultipartFile file, boolean updateSupport) throws Exception
+    {
+        ExcelUtil<CompanyUserImportVO> util = new ExcelUtil<>(CompanyUserImportVO.class);
+        List<CompanyUserImportVO> list = util.importExcel(file.getInputStream());
+        String message = companyUserService.importCompanyUser(list, updateSupport);
+        return AjaxResult.success(message);
+    }
+
+
+    @GetMapping("/importTemplate")
+    public AjaxResult importTemplate()
+    {
+        ExcelUtil<CompanyUserImportVO> util = new ExcelUtil<>(CompanyUserImportVO.class);
+        return util.importTemplateExcel("销售数据");
+    }
+
+
+
+    /**
+     * 根据用户编号获取详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('company:user:query')")
+    @GetMapping(value = { "/", "/{userId}" })
+    public AjaxResult getInfo(@PathVariable(value = "userId", required = false) Long userId)
+    {
+        AjaxResult ajax = AjaxResult.success();
+        CompanyRole companyRoleMap=new CompanyRole();
+
+        CompanyUser userById = companyUserService.selectCompanyUserById(userId);
+        companyRoleMap.setCompanyId(userById.getCompanyId());
+        List<CompanyRole> roles = roleService.selectCompanyRoleList(companyRoleMap);
+
+        ajax.put("roles", CompanyUser.isAdmin(userById.getUserType()) ? roles : roles.stream().filter(r -> r.getRoleKey()!="admin").collect(Collectors.toList()));
+
+        CompanyPost postMap=new CompanyPost();
+        postMap.setCompanyId(userById.getCompanyId());
+        ajax.put("posts", postService.selectCompanyPostList(postMap));
+        if (StringUtils.isNotNull(userId))
+        {
+            CompanyUser companyUser=companyUserService.selectCompanyUserById(userId);
+            ajax.put(AjaxResult.DATA_TAG, companyUser);
+            ajax.put("postIds", postService.selectPostListByUserId(userId));
+            ajax.put("roleIds", roleService.selectRoleListByUserId(userId));
+
+
+        }
+        return ajax;
+    }
+
+    /**
+     * 根据用户编号获取详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('company:user:query')")
+    @GetMapping("/addInfo/{companyId}")
+    public AjaxResult addInfo(@PathVariable(value = "companyId", required = true) Long companyId)
+    {
+        AjaxResult ajax = AjaxResult.success();
+
+        CompanyRole companyRoleMap=new CompanyRole();
+        companyRoleMap.setCompanyId(companyId);
+        List<CompanyRole> roles = roleService.selectCompanyRoleList(companyRoleMap);
+
+        ajax.put("roles", CompanyUser.isAdmin("01") ? roles : roles.stream().filter(r -> r.getRoleKey()!="admin").collect(Collectors.toList()));
+
+        CompanyPost postMap=new CompanyPost();
+        postMap.setCompanyId(companyId);
+        ajax.put("posts", postService.selectCompanyPostList(postMap));
+
+        return ajax;
+    }
+
+    /**
+     * 新增用户
+     */
+    @PreAuthorize("@ss.hasPermi('company:user:add')")
+    @Log(title = "用户管理", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@Validated @RequestBody CompanyUser user)
+    {
+        if (!PatternUtils.checkPassword(user.getPassword())) {
+            return AjaxResult.error("密码格式不正确,需包含字母、数字和特殊字符,长度为 8-20 位");
+        }
+
+        //判断用户数量是否已达到上线
+
+        Integer count=companyUserService.selectCompanyUserCountByCompanyId(user.getCompanyId());
+        Company company=companyService.selectCompanyById(user.getCompanyId());
+
+        if(count>company.getLimitUserCount()){
+            return AjaxResult.error("用户数量已达到上限");
+        }
+        user.setCompanyId(user.getCompanyId());
+        if (UserConstants.NOT_UNIQUE.equals(String.valueOf(companyUserService.checkUserName(user.getUserName()))))
+        {
+            return AjaxResult.error("新增用户'" + user.getUserName() + "'失败,登录账号已存在");
+        }
+        if (!StringUtil.strIsNullOrEmpty(user.getDomain())){
+            user.setDomain(user.getDomain().replaceAll("^[\\s\\u2005]+", ""));
+        }
+
+        user.setCreateBy(SecurityUtils.getUsername());
+        user.setPassword(SecurityUtils.encryptPassword(user.getPassword()));
+        user.setCreateTime(new Date());
+        user.setUserType("01");//一般用户
+        user.setIsAudit(1);
+        return toAjax(companyUserService.insertUser(user));
+    }
+
+    /**
+     * 生成创建销售的二维码
+     */
+
+    @PreAuthorize("@ss.hasPermi('company:user:addCodeUrl')")
+    @Log(title = "生成创建销售的二维码", businessType = BusinessType.INSERT)
+    @PostMapping("/addCodeUrl")
+    public R addCodeUrl( @RequestBody CompanyUserCodeParam user) throws Exception {
+
+        QwCompany qwCompany = iQwCompanyService.getQwCompanyByRedis(user.getCorpId());
+        if (qwCompany == null || qwCompany.getCorpId() == null){
+            return R.error("企业信息不存在");
+        }
+
+        //部门
+        Long deptId = user.getDeptId();
+
+        //角色组
+        Long[] roleIds = user.getRoleIds();
+
+        //区域
+        String addressId = user.getAddressId();
+
+
+        Map<String, Object> stateMap = new HashMap<>();
+        stateMap.put("companyId", user.getCompanyId());
+        stateMap.put("deptId", deptId);
+        stateMap.put("roleIds", roleIds); // 数组可以直接存放
+        stateMap.put("addressId", addressId);
+
+        // 使用JSON库将Map转换为字符串
+        ObjectMapper objectMapper = new ObjectMapper();
+        String status = objectMapper.writeValueAsString(stateMap);
+
+        // 如果要将status作为URL参数传递,记得进行URL编码!
+        String encodedStatus = URLEncoder.encode(status, StandardCharsets.UTF_8.toString());
+
+        String url="https://open.weixin.qq.com/connect/oauth2/authorize?appid="+user.getCorpId()+"&redirect_uri=" +
+                "http://"+qwCompany.getRealmNameUrl()+"/loginqw/pages/companyLogin/index?corpId="+user.getCorpId() +
+                "&response_type=code&scope=snsapi_base&state="+encodedStatus+"&agentid="+qwCompany.getServerAgentId()+"#wechat_redirect";
+
+        R andUpload = QRCodeUtils.createAndUpload(url);
+        return  R.ok().put("data",andUpload);
+    }
+    /**
+     * 修改用户
+     */
+    @PreAuthorize("@ss.hasPermi('company:user:edit')")
+    @Log(title = "用户管理", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@Validated @RequestBody CompanyUser user)
+    {
+
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        user.setUpdateBy(SecurityUtils.getUsername());
+
+        if (!StringUtil.strIsNullOrEmpty(user.getDomain())){
+            user.setDomain(user.getDomain().replaceAll("^[\\s\\u2005]+", ""));
+        }
+
+        CompanyUser companyUser = companyUserService.selectCompanyUserById(user.getUserId());
+
+        if (!companyUser.getUserName().equals(user.getUserName())){
+            if (UserConstants.NOT_UNIQUE.equals(String.valueOf(companyUserService.checkUserName(user.getUserName()))))
+            {
+                return AjaxResult.error("修改员工账号'" + user.getUserName() + "'失败,员工账号已存在");
+            }
+        }
+
+
+        logger.info("用户管理-修改用户:"+user.getUserName()+"/登陆人:"+loginUser.getUser().getUserId()+"/名字:"+loginUser.getUser().getUserName()+"/修改的角色:"+ Arrays.toString(user.getRoleIds()));
+        return toAjax(companyUserService.updateUser(user));
+    }
+
+    /**
+     * 删除用户
+     */
+    @PreAuthorize("@ss.hasPermi('company:user:remove')")
+    @Log(title = "用户管理", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{userIds}")
+    public AjaxResult remove(@PathVariable Long[] userIds)
+    {
+        for (Long userId : userIds) {
+            CompanyUser companyUser = companyUserService.selectCompanyUserById(userId);
+            CompanyUserDelayTime companyUserDelayTime = companyUserDelayTimeService.selectCompanyUserDelayTimeByCompanyUser(companyUser.getCompanyId(),userId);
+            if (ObjectUtil.isNotEmpty(companyUserDelayTime)){
+                companyUserDelayTimeService.deleteCompanyUserDelayTimeById(companyUserDelayTime.getId());
+            }
+        }
+        return toAjax(companyUserService.deleteCompanyUserByIds(userIds));
+    }
+
+    /**
+     * 重置密码
+     */
+    @PreAuthorize("@ss.hasPermi('company:user:edit')")
+    @Log(title = "用户管理", businessType = BusinessType.UPDATE)
+    @PutMapping("/resetPwd")
+    public AjaxResult resetPwd(@RequestBody CompanyUser user)
+    {
+        if (!PatternUtils.checkPassword(user.getPassword())) {
+            return AjaxResult.error("密码格式不正确,需包含字母、数字和特殊字符,长度为 8-20 位");
+        }
+
+        String newPassword = SecurityUtils.encryptPassword(user.getPassword());
+        int i = companyUserService.resetUserPwdByUserId(user.getUserId(), newPassword);
+
+        if (i > 0) {
+            // 更新缓存用户密码
+            redisCache.deleteObject("newCompanyUser:" + user.getCompanyId() + ":" + user.getUserName());
+        }
+
+        return toAjax(i);
+    }
+
+    /**
+     * 状态修改
+     */
+    @PreAuthorize("@ss.hasPermi('company:user:edit')")
+    @Log(title = "用户管理", businessType = BusinessType.UPDATE)
+    @PutMapping("/changeStatus")
+    public AjaxResult changeStatus(@RequestBody CompanyUser user)
+    {
+        //管理员的状态不能修改
+        CompanyUser companyUser=companyUserService.selectCompanyUserById(user.getUserId());
+        if(companyUser.isAdmin()){
+            return AjaxResult.error("不能修改管理员状态");
+        }
+        user.setUpdateBy(SecurityUtils.getUsername());
+        return toAjax(companyUserService.updateCompanyUser(user));
+    }
+
+
+
+
+    /**
+     * 获取区域
+     */
+    @GetMapping("/getCitysAreaList")
+    public R getCitysAreaList(){
+        return R.ok().put("data",companyUserService.getCitysAreaList());
+    }
+
+    /**
+     * 批量修改 销售的所属区域(临时的)
+     */
+    @PostMapping("/updateCompanyUserAreaList")
+    public R updateCompanyUserAreaList(@RequestBody CompanyUserAreaParam param)
+    {
+        return companyUserService.updateCompanyUserAreaList(param);
+    }
+
+
+    @GetMapping("/generateSubDomain")
+    public R generateSubDomain(CompanyUser user){
+        //获取后台配置
+        String json= configService.selectConfigByKey("course.config");
+        CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
+        // 生成二级域名
+        String subDomain = "http://" + DomainUtil.generateSubDomain(config.getCourseDomainName(), 6, String.valueOf(SecurityUtils.getLoginUser().getUser().getUserId()));
+        return  R.ok().put("data",subDomain);
+    }
+
+    @Log(title = "设置是否需要单独注册会员", businessType = BusinessType.UPDATE)
+    @PutMapping("/setRegister")
+    public AjaxResult setIsRegisterMember(@RequestParam Boolean status, @RequestBody List<Long> userIds) {
+        Boolean r = companyUserService.setIsRegisterMember(status, userIds);
+        if (r) {
+            return AjaxResult.success();
+        } else {
+            return AjaxResult.error("操作失败");
+        }
+    }
+
+    @Log(title = "是否允许所有方式注册会员", businessType = BusinessType.UPDATE)
+    @PutMapping("/allowedAllRegister")
+    public AjaxResult isAllowedAllRegister(@RequestParam Boolean status, @RequestBody List<Long> userIds) {
+        Boolean r = companyUserService.isAllowedAllRegister(status, userIds);
+        if (r) {
+            return AjaxResult.success();
+        } else {
+            return AjaxResult.error("操作失败");
+        }
+    }
+    @PostMapping("/common/uploadOSS")
+    public R uploadOSS(@RequestParam("file") MultipartFile file,
+                       @RequestParam("userId") String userId) throws Exception {
+        String url = companyUserService.uploadQrCode(file, userId);
+        return R.ok().put("url", url);
+    }
+
+
+    /**
+     * 销售解除绑定医生
+     */
+    @PreAuthorize("@ss.hasPermi('qw:companyUser:unBindDoctorId')")
+    @Log(title = "销售解除绑定医生", businessType = BusinessType.UPDATE)
+    @GetMapping("/unBindDoctorId/{userId}")
+    public R bindDoctorId(@PathVariable("userId") Long userId){
+        return companyUserService.unBindDoctor(userId);
+    }
+
+    /**
+     * 销售绑定医生
+     */
+    @PreAuthorize("@ss.hasPermi('qw:companyUser:bindDoctorId')")
+    @Log(title = "销售绑定医生", businessType = BusinessType.UPDATE)
+    @PostMapping("/bindDoctorId")
+    public R unBindDoctorId(@RequestBody CompanyUser companyUser){
+        return companyUserService.bindDoctor(companyUser);
+    }
+
+    /**
+     * 批量修改角色
+     * @param batchUserRolesVO
+     */
+    @PreAuthorize("@ss.hasPermi('company:user:edit')")
+    @Log(title = "批量修改角色", businessType = BusinessType.UPDATE)
+    @PostMapping("/updateBatchUserRoles")
+    public R updateBatchUserRoles(@RequestBody BatchUserRolesVO batchUserRolesVO){
+        Assert.notEmpty(batchUserRolesVO.getRoleIds(), "角色不能为空");
+        Assert.notEmpty(batchUserRolesVO.getUserIds(), "用户不能为空");
+        return companyUserService.updateBatchUserRoles(batchUserRolesVO);
+    }
+
+
+    @ApiOperation("校验客服是否注册新的im")
+    @PostMapping("/accountCheck")
+    public R accountCheck(@RequestBody Map<String, String> userIdMap){
+        //获取管理员token
+        String userId = userIdMap.get("userId");
+        String adminToken = openIMService.getAdminToken();
+        JSONObject requestBody = new JSONObject();
+        // 解析响应
+        if (StringUtils.isNotEmpty(adminToken)) {
+            //查询用户是否注册
+            ArrayList<String> userIds = new ArrayList<>();
+            requestBody = new JSONObject();
+            userIds.add(userId);
+            requestBody.put("checkUserIDs", userIds);
+            String body = HttpRequest.post(IMConfig.URL+"/user/account_check")
+                    .header("operationID", String.valueOf(System.currentTimeMillis()))
+                    .header("token", adminToken)
+                    .body(requestBody.toString())
+                    .execute()
+                    .body();
+            JSONObject jsonObject = new JSONObject(body);
+            JSONArray results = jsonObject.getJSONObject("data").getJSONArray("results");
+            if (results != null && results.length() > 0) {
+                JSONObject resultObj = results.getJSONObject(0);
+                int accountStatus = resultObj.getInt("accountStatus");
+                //未注册自动注册
+                if (accountStatus==0){
+                    String s = userId.replaceFirst("^C", "");
+                    CompanyUser companyUser = companyUserService.selectCompanyUserById(Long.parseLong(s));
+                    if (null==companyUser){
+                        return R.error("用户不存在");
+                    }
+                    ArrayList<Object> users = new ArrayList<>();
+                    HashMap<String, String> map = new HashMap<>();
+                    map.put("userID",userId);
+                    map.put("nickname",companyUser.getNickName());
+                    map.put("faceURL",companyUser.getAvatar());
+                    users.add(map);
+                    requestBody = new JSONObject();
+                    //userIds.add(userId);
+                    requestBody.put("users", users);
+                    String registerBody = HttpRequest.post(IMConfig.URL+"/user/user_register")
+                            .header("operationID", String.valueOf(System.currentTimeMillis()))
+                            .header("token", adminToken).body(requestBody.toString()).execute().body();
+                }
+            } else {
+                return R.error("返回结果为空");
+            }
+           /* HashMap<String, String> tokenMap = new HashMap<>();
+            tokenMap.put("platformID","1");
+            tokenMap.put("userID",userId);*/
+            requestBody = new JSONObject();
+            requestBody.put("platformID",5);
+            requestBody.put("userID",userId);
+            String body1 = HttpRequest.post(IMConfig.URL+"/auth/get_user_token")
+                    .header("operationID", String.valueOf(System.currentTimeMillis()))
+                    .header("token", adminToken)
+                    .body(requestBody.toString()).execute().body();
+            JSONObject userJson = new JSONObject(body1);
+            JSONObject userData = userJson.getJSONObject("data");
+            String userToken = userData.getString("token");
+            return R.ok().put("token", userToken);
+        } else {
+            return R.error("获取管理员token失败");
+        }
+    }
+    @ApiOperation("添加好友")
+    @PostMapping("/importFriend")
+    public R importFriend(@RequestBody HashMap<String,Object> map){
+        String ownerUserID = (String) map.get("ownerUserID");
+        List<String> friendUserIDs = (List<String>)map.get("friendUserIDs");
+        OpenImResponseDTO openImResponseDTO = openIMService.importFriend(ownerUserID, friendUserIDs);
+        return R.ok().put("data",openImResponseDTO);
+    }
+}

+ 41 - 0
fs-admin/src/main/java/com/fs/course/business/FsVideoResourceBusinessService.java

@@ -0,0 +1,41 @@
+package com.fs.course.business;
+
+import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
+import com.fs.course.domain.FsUserCourseVideo;
+import com.fs.course.domain.FsVideoResource;
+import com.fs.course.service.IFsUserCourseVideoService;
+import com.fs.course.service.IFsVideoResourceService;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+public class FsVideoResourceBusinessService {
+
+    private final IFsVideoResourceService fsVideoResourceService;
+
+    private final IFsUserCourseVideoService fsUserCourseVideoService;
+
+    public FsVideoResourceBusinessService(IFsVideoResourceService fsVideoResourceService,
+                                          IFsUserCourseVideoService fsUserCourseVideoService) {
+        this.fsVideoResourceService = fsVideoResourceService;
+        this.fsUserCourseVideoService = fsUserCourseVideoService;
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    public void edit(FsVideoResource fsVideoResource) {
+        FsVideoResource oldResource = fsVideoResourceService.getById(fsVideoResource.getId());
+        fsVideoResourceService.updateById(fsVideoResource);
+        fsUserCourseVideoService.update(new UpdateWrapper<FsUserCourseVideo>()
+                .eq("video_url", oldResource.getVideoUrl())
+                .set("video_url", fsVideoResource.getVideoUrl())
+                .set("line_one", fsVideoResource.getLine1())
+                .set("line_two", fsVideoResource.getLine2())
+                .set("line_three", fsVideoResource.getLine3())
+                .set("duration", fsVideoResource.getDuration())
+                .set("file_size", fsVideoResource.getFileSize())
+                .set("file_key", fsVideoResource.getFileKey())
+                .set("file_name", fsVideoResource.getFileName())
+                .set("thumbnail", fsVideoResource.getThumbnail())
+        );
+    }
+}

+ 41 - 0
fs-admin/src/main/java/com/fs/course/controller/CourseRedPacketStatisticsController.java

@@ -0,0 +1,41 @@
+package com.fs.course.controller;
+
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.R;
+import com.fs.course.dto.CourseRedPacketStatisticsDTO;
+import com.fs.course.param.CourseRedPacketStatisticsParam;
+import com.fs.course.service.CourseRedPacketStatisticsService;
+import com.github.pagehelper.PageInfo;
+import io.swagger.annotations.ApiModelProperty;
+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;
+
+/**
+ * @description: 看客红包发送统计
+ * @author: Xgb
+ * @createDate: 2025/10/14
+ * @version: 1.0
+ */
+@RestController
+@RequestMapping("/course/courseRedPacketStatistics")
+public class CourseRedPacketStatisticsController extends BaseController {
+
+    @Autowired
+    private CourseRedPacketStatisticsService courseRedPacketStatisticsService;
+
+    @ApiModelProperty("看客红包发送统计")
+    @GetMapping("/list")
+    public R list(CourseRedPacketStatisticsParam param) {
+
+        startPage();
+        // 看客红包发送统计
+        List<CourseRedPacketStatisticsDTO> list = courseRedPacketStatisticsService.statistics(param);
+
+        return R.ok().put("data", new PageInfo<>(list));
+    }
+
+}

+ 18 - 6
fs-admin/src/main/java/com/fs/course/controller/FsCourseAnswerLogsController.java

@@ -3,6 +3,7 @@ package com.fs.course.controller;
 import com.fs.common.annotation.Log;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.domain.R;
 import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
 import com.fs.common.utils.poi.ExcelUtil;
@@ -10,6 +11,8 @@ import com.fs.course.domain.FsCourseAnswerLogs;
 import com.fs.course.param.FsCourseAnswerLogsParam;
 import com.fs.course.service.IFsCourseAnswerLogsService;
 import com.fs.course.vo.FsCourseAnswerLogsListVO;
+import com.github.pagehelper.PageHelper;
+import com.github.pagehelper.PageInfo;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
@@ -35,17 +38,26 @@ public class FsCourseAnswerLogsController extends BaseController
      * 查询答题日志列表
      */
     @PreAuthorize("@ss.hasPermi('course:courseAnswerLog:list')")
-    @GetMapping("/list")
-    public TableDataInfo list(FsCourseAnswerLogsParam param)
+    @PostMapping("/list")
+    public R list(@RequestBody  FsCourseAnswerLogsParam param)
     {
-        startPage();
+        if(param.getPageNum() == null) {
+            param.setPageNum(1);
+        }
+        if(param.getPageSize() == null) {
+            param.setPageSize(10);
+        }
 
         if (param.getPhoneMk() != null && param.getPhoneMk() != "") {
             param.setPhone(encryptPhone(param.getPhoneMk()));
         }
 
+
+        PageHelper.startPage(param.getPageNum(), param.getPageSize());
+
         List<FsCourseAnswerLogsListVO> list = fsCourseAnswerLogsService.selectFsCourseAnswerLogsListVO(param);
-        return getDataTable(list);
+
+        return R.ok().put("data",new PageInfo<>(list));
     }
 
     /**
@@ -53,8 +65,8 @@ public class FsCourseAnswerLogsController extends BaseController
      */
     @PreAuthorize("@ss.hasPermi('course:courseAnswerLog:export')")
     @Log(title = "答题日志", businessType = BusinessType.EXPORT)
-    @GetMapping("/export")
-    public AjaxResult export(FsCourseAnswerLogsParam param)
+    @PostMapping("/export")
+    public AjaxResult export(@RequestBody FsCourseAnswerLogsParam param)
     {
         if (param.getPhoneMk() != null && param.getPhoneMk() != "") {
             param.setPhone(encryptPhone(param.getPhoneMk()));

+ 38 - 0
fs-admin/src/main/java/com/fs/course/controller/FsCourseFinishTempController.java

@@ -6,14 +6,22 @@ import com.fs.common.core.domain.AjaxResult;
 import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
 import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.company.service.impl.CompanyUserServiceImpl;
+import com.fs.company.vo.DocCompanyUserVO;
 import com.fs.course.domain.FsCourseFinishTemp;
 import com.fs.course.service.IFsCourseFinishTempService;
 import com.fs.course.vo.FsCourseFinishTempListVO;
+import com.fs.sop.domain.QwSopTemp;
+import com.fs.voice.utils.StringUtil;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
 
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
 
 /**
  * 完课模板Controller
@@ -27,6 +35,8 @@ public class FsCourseFinishTempController extends BaseController
 {
     @Autowired
     private IFsCourseFinishTempService fsCourseFinishTempService;
+    @Autowired
+    private CompanyUserServiceImpl companyUserService;
 
     /**
      * 查询完课模板列表
@@ -37,6 +47,34 @@ public class FsCourseFinishTempController extends BaseController
     {
         startPage();
         List<FsCourseFinishTempListVO> list = fsCourseFinishTempService.selectFsCourseFinishTempListVO(fsCourseFinishTemp);
+        // 收集所有需要查询的用户ID
+        Set<Long> userIds = list.stream()
+                .map(FsCourseFinishTempListVO::getCreateBy)
+                .filter(str -> !StringUtil.strIsNullOrEmpty(str)) // 取反,保留非空值
+                .map(Long::valueOf)
+                .collect(Collectors.toSet());
+
+        if (!userIds.isEmpty()){
+            // 批量查询用户信息
+            Map<Long, DocCompanyUserVO> userMap = companyUserService
+                    .selectDocCompanyUserListByUserIds(userIds)
+                    .stream()
+                    .collect(Collectors.toMap(DocCompanyUserVO::getUserId, Function.identity()));
+
+
+            list.forEach(item->{
+
+                if (!StringUtil.strIsNullOrEmpty(item.getCreateBy())) {
+                    DocCompanyUserVO user = userMap.get(Long.valueOf(item.getCreateBy()));
+                    if (user != null) {
+                        item.setCreateByName(user.getNickName());
+                        item.setCreateByDeptName(user.getDeptName());
+                    }
+                }
+
+            });
+        }
+
         return getDataTable(list);
     }
 

+ 70 - 5
fs-admin/src/main/java/com/fs/course/controller/FsCoursePlaySourceConfigController.java

@@ -1,21 +1,30 @@
 package com.fs.course.controller;
 
+import cn.hutool.json.JSONUtil;
 import com.baomidou.mybatisplus.core.conditions.Wrapper;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+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.fs.common.annotation.Log;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.AjaxResult;
 import com.fs.common.core.domain.R;
+import com.fs.common.core.domain.R;
+import com.fs.common.core.domain.model.LoginUser;
 import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.ServletUtils;
 import com.fs.common.utils.bean.BeanUtils;
+import com.fs.course.config.CourseConfig;
 import com.fs.course.domain.FsCoursePlaySourceConfig;
 import com.fs.course.param.FsCoursePlaySourceConfigCreateParam;
 import com.fs.course.param.FsCoursePlaySourceConfigEditParam;
 import com.fs.course.service.IFsCoursePlaySourceConfigService;
 import com.fs.course.vo.FsCoursePlaySourceConfigVO;
+import com.fs.framework.web.service.TokenService;
+import com.fs.system.service.ISysConfigService;
 import com.github.pagehelper.PageHelper;
 import lombok.AllArgsConstructor;
 import org.springframework.security.access.prepost.PreAuthorize;
@@ -31,16 +40,36 @@ import java.util.*;
 public class FsCoursePlaySourceConfigController extends BaseController {
 
     private final IFsCoursePlaySourceConfigService fsCoursePlaySourceConfigService;
+    private final TokenService tokenService;
+    private final ISysConfigService configService;
 
-    @PreAuthorize("@ss.hasPermi('course:playSourceConfig:list')")
+//    @PreAuthorize("@ss.hasPermi('course:playSourceConfig:list')")
     @GetMapping("/list")
     public TableDataInfo list(@RequestParam(required = false) String name,
                               @RequestParam(required = false) String appid,
+                              @RequestParam(required = false) Integer isMall,
                               @RequestParam(required = false, defaultValue = "1") Integer pageNum,
-                              @RequestParam(required = false, defaultValue = "10") Integer pageSize) {
+                              @RequestParam(required = false, defaultValue = "10") Integer pageSize,
+                              @RequestParam(required = false) Long companyId
+    ) {
         Map<String, Object> params = new HashMap<>();
         params.put("name", name);
         params.put("appid", appid);
+        params.put("isMall", isMall);
+        params.put("companyId", companyId);
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        String json = configService.selectConfigByKey("course.config");
+        CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
+        Long userId = null;
+        Long deptId = null;
+        if(!loginUser.isAdmin() && config.getDept() != null && config.getDept()){
+            deptId = loginUser.getDeptId();
+            if(config.getDept() == null || !config.getDept()){
+                userId = loginUser.getUserId();
+            }
+        }
+        params.put("userId", userId);
+        params.put("deptId", deptId);
 
         PageHelper.startPage(pageNum, pageSize);
         List<FsCoursePlaySourceConfigVO> list = fsCoursePlaySourceConfigService.selectCoursePlaySourceConfigVOListByMap(params);
@@ -70,8 +99,10 @@ public class FsCoursePlaySourceConfigController extends BaseController {
         if (fsCoursePlaySourceConfigService.count(queryWrapper) > 0) {
             return AjaxResult.error("appid已存在");
         }
-
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         FsCoursePlaySourceConfig config = new FsCoursePlaySourceConfig();
+        config.setCreateUserId(loginUser.getUserId());
+        config.setCreateDeptId(loginUser.getDeptId());
         BeanUtils.copyProperties(param, config);
 
         config.setIsDel(0);
@@ -104,6 +135,29 @@ public class FsCoursePlaySourceConfigController extends BaseController {
         return AjaxResult.success();
     }
 
+    @PreAuthorize("@ss.hasPermi('course:playSourceConfig:bind')")
+    @Log(title = "绑定支付配置", businessType = BusinessType.UPDATE)
+    @PutMapping("/updateBindConfig")
+    public AjaxResult updateBindConfig(@RequestBody FsCoursePlaySourceConfigEditParam param) {
+
+        FsCoursePlaySourceConfig update =new FsCoursePlaySourceConfig();
+        update.setId(param.getId());
+        update.setMerchantConfigId(param.getMerchantConfigId());
+        fsCoursePlaySourceConfigService.updateById(update);
+        return AjaxResult.success();
+    }
+
+    @PreAuthorize("@ss.hasPermi('course:playSourceConfig:unbind')")
+    @Log(title = "解绑支付配置", businessType = BusinessType.UPDATE)
+    @PutMapping("/updateUnbindConfig")
+    public AjaxResult updateUnbindConfig(@RequestBody FsCoursePlaySourceConfigEditParam param) {
+        LambdaUpdateWrapper<FsCoursePlaySourceConfig> updateWrapper = Wrappers.lambdaUpdate();
+        updateWrapper.eq(FsCoursePlaySourceConfig::getId, param.getId())
+                .set(FsCoursePlaySourceConfig::getMerchantConfigId, null);
+        fsCoursePlaySourceConfigService.update(updateWrapper);
+        return AjaxResult.success();
+    }
+
     @PreAuthorize("@ss.hasPermi('course:playSourceConfig:remove')")
     @Log(title = "点播播放源配置", businessType = BusinessType.DELETE)
     @DeleteMapping("/{ids}")
@@ -114,8 +168,19 @@ public class FsCoursePlaySourceConfigController extends BaseController {
         fsCoursePlaySourceConfigService.update(updateWrapper);
         return AjaxResult.success();
     }
+
     @GetMapping("/listAll")
-    public R listAll() {
-        return R.ok().put("data", fsCoursePlaySourceConfigService.list(new QueryWrapper<FsCoursePlaySourceConfig>().eq("is_del", 0)));
+    public R listAll(Long companyId) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        String json = configService.selectConfigByKey("course.config");
+        CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
+        QueryWrapper<FsCoursePlaySourceConfig> queryWrapper = new QueryWrapper<FsCoursePlaySourceConfig>().eq("is_del", 0);
+        if(!loginUser.isAdmin() && config.getDept() != null && config.getDept()){
+            queryWrapper.eq("create_user_id", loginUser.getUserId()).eq(config.getDept() == null || !config.getDept(), "create_dept_id", loginUser.getDeptId());
+        }
+        if(companyId != null){
+            queryWrapper.and(e -> e.eq("company_id", companyId).or().isNull("company_id"));
+        }
+        return R.ok().put("data", fsCoursePlaySourceConfigService.list(queryWrapper));
     }
 }

+ 35 - 8
fs-admin/src/main/java/com/fs/course/controller/FsCourseQuestionBankController.java

@@ -13,6 +13,7 @@ import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.course.config.CourseConfig;
 import com.fs.course.domain.FsCourseQuestionBank;
 import com.fs.course.dto.FsCourseQuestionBankImportDTO;
+import com.fs.course.dto.ImportResultDTO;
 import com.fs.course.service.IFsCourseQuestionBankService;
 import com.fs.framework.web.service.TokenService;
 import com.fs.system.service.ISysConfigService;
@@ -21,7 +22,9 @@ import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
 import org.springframework.web.multipart.MultipartFile;
 
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 /**
  * 题库Controller
@@ -80,6 +83,15 @@ public class FsCourseQuestionBankController extends BaseController
         return util.exportExcel(list, "题库数据");
     }
 
+    @PreAuthorize("@ss.hasPermi('course:courseQuestionBank:exportFail')")
+    @Log(title = "题库", businessType = BusinessType.EXPORT)
+    @PostMapping("/exportFail")
+    public AjaxResult export( @RequestBody List<FsCourseQuestionBankImportDTO> list)
+    {
+        ExcelUtil<FsCourseQuestionBankImportDTO> util = new ExcelUtil<>(FsCourseQuestionBankImportDTO.class);
+        return util.exportExcel(list, "题库错误数据");
+    }
+
     /**
      * 获取题库详细信息
      */
@@ -144,19 +156,34 @@ public class FsCourseQuestionBankController extends BaseController
     @PreAuthorize("@ss.hasPermi('course:courseQuestionBank:importData')")
     @PostMapping("/importData")
     public AjaxResult importData(MultipartFile file) throws Exception {
-        ExcelUtil<FsCourseQuestionBankImportDTO> util = new ExcelUtil<>(FsCourseQuestionBankImportDTO.class);
-        List<FsCourseQuestionBankImportDTO> list = util.importExcel(file.getInputStream());
+
+        ExcelUtil<FsCourseQuestionBankImportDTO> util =
+                new ExcelUtil<>(FsCourseQuestionBankImportDTO.class);
+        List<FsCourseQuestionBankImportDTO> list =
+                util.importExcel(file.getInputStream());
 
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         Long userId = loginUser.getUser().getUserId();
+
+        // 读取配置
         String json = configService.selectConfigByKey("course.config");
         CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
-        if (ObjectUtil.isNotEmpty(config.getIsBound())&&config.getIsBound()){
-            String message = fsCourseQuestionBankService.importData(list, loginUser.getUser().getNickName(),userId);
-            return AjaxResult.success(message);
-        }
-        String message = fsCourseQuestionBankService.importData(list, loginUser.getUser().getNickName(),null);
-        return AjaxResult.success(message);
+
+        // 绑定状态控制 userId
+        Long finalUserId = (ObjectUtil.isNotEmpty(config.getIsBound()) && config.getIsBound())
+                ? userId
+                : null;
+
+        // 调用 service
+        ImportResultDTO result =
+                fsCourseQuestionBankService.importData(list, loginUser.getUser().getNickName(), finalUserId);
+
+        // 返回 message + failList
+        Map<String, Object> resp = new HashMap<>();
+        resp.put("message", result.buildResultMessage());
+        resp.put("failList", result.getFailureList());
+
+        return AjaxResult.success(resp);
     }
 
     @GetMapping(value = "/getByIds")

+ 77 - 20
fs-admin/src/main/java/com/fs/course/controller/FsCourseRedPacketLogController.java

@@ -1,24 +1,27 @@
 package com.fs.course.controller;
 
-import java.util.List;
-
+import java.util.*;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.json.JSONUtil;
 import com.fs.common.core.domain.R;
+import com.fs.common.core.domain.model.LoginUser;
+import com.fs.common.utils.ServletUtils;
+import com.fs.course.config.CourseConfig;
+import com.fs.course.domain.FsUserCoursePeriod;
 import com.fs.course.mapper.FsUserCourseMapper;
 import com.fs.course.mapper.FsUserCourseVideoMapper;
 import com.fs.course.param.FsCourseRedPacketLogParam;
+import com.fs.course.service.IFsUserCoursePeriodService;
 import com.fs.course.vo.FsCourseRedPacketLogListPVO;
+import com.fs.framework.web.service.TokenService;
 import com.fs.his.utils.PhoneUtil;
 import com.fs.his.vo.OptionsVO;
+import com.fs.system.service.ISysConfigService;
+import com.github.pagehelper.PageHelper;
+import com.github.pagehelper.PageInfo;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.PutMapping;
-import org.springframework.web.bind.annotation.DeleteMapping;
-import org.springframework.web.bind.annotation.PathVariable;
-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 com.fs.common.annotation.Log;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.AjaxResult;
@@ -42,31 +45,73 @@ public class FsCourseRedPacketLogController extends BaseController
 {
     @Autowired
     private IFsCourseRedPacketLogService fsCourseRedPacketLogService;
+
+    @Autowired
+    private IFsUserCoursePeriodService fsUserCoursePeriodService;
     @Autowired
     FsUserCourseMapper fsUserCourseMapper;
     @Autowired
+    TokenService tokenService;
+    @Autowired
+    ISysConfigService configService;
+    @Autowired
     FsUserCourseVideoMapper fsUserCourseVideoMapper;
-
-
     /**
      * 查询短链课程看课记录列表
      */
     @PreAuthorize("@ss.hasPermi('course:courseRedPacketLog:list')")
-    @GetMapping("/list")
-    public TableDataInfo list(FsCourseRedPacketLogParam fsCourseRedPacketLog)
+    @PostMapping("/list")
+    public R list(@RequestBody FsCourseRedPacketLogParam fsCourseRedPacketLog)
     {
-        startPage();
+        if(fsCourseRedPacketLog.getPageNum() == null) {
+            fsCourseRedPacketLog.setPageNum(1);
+        }
+        if(fsCourseRedPacketLog.getPageSize() == null) {
+            fsCourseRedPacketLog.setPageSize(10);
+        }
+
 
         if (fsCourseRedPacketLog.getPhoneMk() != null && fsCourseRedPacketLog.getPhoneMk() != "") {
             fsCourseRedPacketLog.setPhone(encryptPhone(fsCourseRedPacketLog.getPhoneMk()));
         }
+        PageHelper.startPage(fsCourseRedPacketLog.getPageNum(), fsCourseRedPacketLog.getPageSize());
 
         List<FsCourseRedPacketLogListPVO> list = fsCourseRedPacketLogService.selectFsCourseRedPacketLogListVO(fsCourseRedPacketLog);
         for (FsCourseRedPacketLogListPVO fsCourseRedPacketLogListPVO : list) {
+            if (ObjectUtil.isNotEmpty(fsCourseRedPacketLogListPVO.getPeriodId())){
+                FsUserCoursePeriod fsUserCoursePeriod = fsUserCoursePeriodService.selectFsUserCoursePeriodById(fsCourseRedPacketLogListPVO.getPeriodId().longValue());
+                fsCourseRedPacketLogListPVO.setPeriodName(fsUserCoursePeriod.getPeriodName());
+            }
+            fsCourseRedPacketLogListPVO.setPhone(PhoneUtil.decryptAutoPhoneMk(fsCourseRedPacketLogListPVO.getPhone()));
+        }
+        return R.ok().put("data",new PageInfo<>(list));
+    }
 
+    /**
+     * 查询短链课程看课记录列表分页
+     * 与上面请求方式不同,list集合数据在get传输掉包
+     */
+    @PreAuthorize("@ss.hasPermi('course:courseRedPacketLog:pageList')")
+    @PostMapping("/pageList")
+    public R pageList(@RequestBody FsCourseRedPacketLogParam fsCourseRedPacketLog)
+    {
+
+        if (fsCourseRedPacketLog.getPhoneMk() != null && fsCourseRedPacketLog.getPhoneMk() != "") {
+            fsCourseRedPacketLog.setPhone(encryptPhone(fsCourseRedPacketLog.getPhoneMk()));
+        }
+
+        PageHelper.startPage(fsCourseRedPacketLog.getPageNum(), fsCourseRedPacketLog.getPageSize());
+
+        List<FsCourseRedPacketLogListPVO> list = fsCourseRedPacketLogService.selectFsCourseRedPacketLogListVO(fsCourseRedPacketLog);
+        for (FsCourseRedPacketLogListPVO fsCourseRedPacketLogListPVO : list) {
+            if (ObjectUtil.isNotEmpty(fsCourseRedPacketLogListPVO.getPeriodId())){
+                FsUserCoursePeriod fsUserCoursePeriod = fsUserCoursePeriodService.selectFsUserCoursePeriodById(fsCourseRedPacketLogListPVO.getPeriodId().longValue());
+                fsCourseRedPacketLogListPVO.setPeriodName(fsUserCoursePeriod.getPeriodName());
+            }
             fsCourseRedPacketLogListPVO.setPhone(PhoneUtil.decryptAutoPhoneMk(fsCourseRedPacketLogListPVO.getPhone()));
         }
-        return getDataTable(list);
+
+        return R.ok().put("data", new PageInfo<>(list));
     }
 
     /**
@@ -74,15 +119,18 @@ public class FsCourseRedPacketLogController extends BaseController
      */
     @PreAuthorize("@ss.hasPermi('course:courseRedPacketLog:export')")
     @Log(title = "短链课程看课记录", businessType = BusinessType.EXPORT)
-    @GetMapping("/export")
-    public AjaxResult export(FsCourseRedPacketLogParam fsCourseRedPacketLog)
+    @PostMapping("/export")
+    public AjaxResult export(@RequestBody FsCourseRedPacketLogParam fsCourseRedPacketLog)
     {
         if (fsCourseRedPacketLog.getPhoneMk()!=null&&fsCourseRedPacketLog.getPhoneMk()!=""){
             fsCourseRedPacketLog.setPhone(encryptPhone(fsCourseRedPacketLog.getPhoneMk()));
         }
         List<FsCourseRedPacketLogListPVO> list = fsCourseRedPacketLogService.selectFsCourseRedPacketLogListVO(fsCourseRedPacketLog);
         for (FsCourseRedPacketLogListPVO fsCourseRedPacketLogListPVO : list) {
-
+            if (ObjectUtil.isNotEmpty(fsCourseRedPacketLogListPVO.getPeriodId())){
+                FsUserCoursePeriod fsUserCoursePeriod = fsUserCoursePeriodService.selectFsUserCoursePeriodById(fsCourseRedPacketLogListPVO.getPeriodId().longValue());
+                fsCourseRedPacketLogListPVO.setPeriodName(fsUserCoursePeriod.getPeriodName());
+            }
             fsCourseRedPacketLogListPVO.setPhone(PhoneUtil.decryptAutoPhoneMk(fsCourseRedPacketLogListPVO.getPhone()));
         }
         ExcelUtil<FsCourseRedPacketLogListPVO> util = new ExcelUtil<FsCourseRedPacketLogListPVO>(FsCourseRedPacketLogListPVO.class);
@@ -135,7 +183,16 @@ public class FsCourseRedPacketLogController extends BaseController
     @GetMapping("/courseList")
     public R courseList()
     {
-        List<OptionsVO> optionsVOS = fsUserCourseMapper.selectFsUserCourseAllList();
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long userId = loginUser.getUser().getUserId();
+        String json = configService.selectConfigByKey("course.config");
+        CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
+        List<OptionsVO> optionsVOS;
+        if (ObjectUtil.isNotEmpty(config.getIsBound())&&config.getIsBound()){
+            optionsVOS = fsUserCourseMapper.selectFsUserCourseAllListByUserId(userId);
+        }else{
+            optionsVOS = fsUserCourseMapper.selectFsUserCourseAllList();
+        }
         return R.ok().put("list", optionsVOS);
     }
 

+ 35 - 1
fs-admin/src/main/java/com/fs/course/controller/FsCourseTrafficLogController.java

@@ -9,7 +9,9 @@ import com.fs.common.core.domain.R;
 import com.fs.common.exception.CustomException;
 import com.fs.course.param.FsCourseTrafficLogParam;
 import com.fs.course.param.InternetTrafficParam;
+import com.fs.course.param.StatisticsSummaryParam;
 import com.fs.course.vo.FsCourseTrafficLogListVO;
+import com.fs.course.vo.StatisticsSummaryVO;
 import com.fs.qw.param.QwWatchLogStatisticsListParam;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -70,7 +72,11 @@ public class FsCourseTrafficLogController extends BaseController
             param.setYear(Integer.parseInt(parts[0]));
             param.setMonth(Integer.parseInt(parts[1]));
         }
-        List<FsCourseTrafficLogListVO> list = fsCourseTrafficLogService.selectTrafficByCompany(param);
+        List<FsCourseTrafficLogListVO> list = fsCourseTrafficLogService.selectTrafficNew(param);
+        //格式化每个记录的流量为 GB 字符串
+        for (FsCourseTrafficLogListVO vo : list) {
+            vo.formatTraffic();
+        }
         ExcelUtil<FsCourseTrafficLogListVO> util = new ExcelUtil<FsCourseTrafficLogListVO>(FsCourseTrafficLogListVO.class);
         return util.exportExcel(list, "短链课程流量记录数据");
     }
@@ -131,4 +137,32 @@ public class FsCourseTrafficLogController extends BaseController
         return R.ok().put("data", null);  // 返回计算结果
     }
 
+    /**
+     * 流量统计汇总
+     * @param param
+     * @return
+     */
+    @PreAuthorize("@ss.hasPermi('course:courseTrafficLog:statistics')")
+    @GetMapping("/statisticsSummaryList")
+    public TableDataInfo statisticsSummaryList(StatisticsSummaryParam param)
+    {
+        List<StatisticsSummaryVO> list = fsCourseTrafficLogService.getStatisticsSummaryList(param);
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出汇总统计报表
+     * @param param
+     * @return
+     */
+    @PreAuthorize("@ss.hasPermi('course:courseTrafficLog:statisticsExport')")
+    @GetMapping("/exportStatisticsSummary")
+    public AjaxResult exportStatisticsSummary(StatisticsSummaryParam param)
+    {
+        List<StatisticsSummaryVO> list = fsCourseTrafficLogService.getStatisticsSummaryListNotPage(param);
+        ExcelUtil<StatisticsSummaryVO> util = new ExcelUtil<StatisticsSummaryVO>(StatisticsSummaryVO.class);
+        return util.exportExcel(list, "看课流量统计汇总");
+
+    }
+
 }

+ 70 - 8
fs-admin/src/main/java/com/fs/course/controller/FsCourseWatchLogController.java

@@ -2,15 +2,24 @@ package com.fs.course.controller;
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
 
+import cn.hutool.core.collection.CollectionUtil;
 import com.fs.common.constant.HttpStatus;
+import com.fs.common.core.domain.R;
 import com.fs.common.exception.CustomException;
+import com.fs.common.utils.ServletUtils;
+import com.fs.course.param.FsCourseOverParam;
 import com.fs.course.param.FsCourseWatchLogListParam;
 import com.fs.course.param.FsCourseWatchLogStatisticsListParam;
+import com.fs.course.vo.FsCourseOverVO;
 import com.fs.course.vo.FsCourseWatchLogListVO;
 import com.fs.course.vo.FsCourseWatchLogStatisticsListVO;
 import com.fs.qw.param.QwWatchLogStatisticsListParam;
 import com.fs.qw.service.IQwWatchLogService;
+import com.github.pagehelper.PageHelper;
+import com.github.pagehelper.PageInfo;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.GetMapping;
@@ -53,10 +62,29 @@ public class FsCourseWatchLogController extends BaseController
     public TableDataInfo list(FsCourseWatchLogListParam param)
     {
         startPage();
+        if(CollectionUtil.isNotEmpty(param.getUserIds())){
+            param.setUserIds(param.getUserIds().stream()
+                    .filter(userId -> userId != null && userId.startsWith("user_"))
+                    .map(userId -> userId.substring(5))
+                    .collect(Collectors.toList())
+            );
+        }
         List<FsCourseWatchLogListVO> list = fsCourseWatchLogService.selectFsCourseWatchLogListVO(param);
         return getDataTable(list);
     }
 
+    /**
+     * 查询短链课程看课记录列表
+     */
+    @PreAuthorize("@ss.hasPermi('course:courseWatchLog:pageList')")
+    @PostMapping("/pageList")
+    public R pageList(@RequestBody FsCourseWatchLogListParam param)
+    {
+        PageHelper.startPage(param.getPageNum(), param.getPageSize());
+        List<FsCourseWatchLogListVO> list = fsCourseWatchLogService.selectFsCourseWatchLogListVO(param);
+        return R.ok().put("data", new PageInfo<>(list));
+    }
+
     @GetMapping("/qwWatchLogAllStatisticsList")
     public TableDataInfo qwWatchLogAllStatisticsList(QwWatchLogStatisticsListParam param)
     {
@@ -100,22 +128,45 @@ public class FsCourseWatchLogController extends BaseController
         if (param.getSTime()==null||param.getETime()==null){
             throw new CustomException("必须选择开始时间和结束时间!");
         }
+        startPage();
+        List<FsCourseWatchLogStatisticsListVO> list = fsCourseWatchLogService.selectFsCourseWatchLogStatisticsListVONew(param);
+
+        return getDataTable(list);
+    }
+
+    /**
+     * 会员看课统计导出
+     */
+    @PreAuthorize("@ss.hasPermi('course:courseWatchLog:statisticsExport')")
+    @Log(title = "会员看课统计导出", businessType = BusinessType.EXPORT)
+    @PostMapping("/statisticsExport")
+    public AjaxResult statisticsExport(@RequestBody FsCourseWatchLogStatisticsListParam param)
+    {
+
+        // 如果看指定用户就不用设置公司
+        if(param.getCompanyId() == null){
+            if(param.getUserId() == null) {
+                throw new CustomException("查看公司或者用户必填!");
+            }
+        }
+        if (param.getSTime()==null||param.getETime()==null){
+            throw new CustomException("必须选择开始时间和结束时间!");
+        }
+
         List<FsCourseWatchLogStatisticsListVO> list = fsCourseWatchLogService.selectFsCourseWatchLogStatisticsListVONew(param);
-        TableDataInfo rspData = new TableDataInfo();
-        rspData.setCode(HttpStatus.SUCCESS);
-        rspData.setMsg("查询成功");
-        rspData.setRows(list);
-        rspData.setTotal(fsCourseWatchLogService.selectFsCourseWatchLogStatisticsListVONewCount(param));
-        return rspData;
+
+        ExcelUtil<FsCourseWatchLogStatisticsListVO> util = new ExcelUtil<FsCourseWatchLogStatisticsListVO>(FsCourseWatchLogStatisticsListVO.class);
+        return util.exportExcel(list, "会员看课统计");
     }
 
+
     /**
      * 导出短链课程看课记录列表
      */
     @PreAuthorize("@ss.hasPermi('course:courseWatchLog:export')")
     @Log(title = "短链课程看课记录", businessType = BusinessType.EXPORT)
-    @GetMapping("/export")
-    public AjaxResult export(FsCourseWatchLogListParam param)
+    @PostMapping("/export")
+    public AjaxResult export(@RequestBody FsCourseWatchLogListParam param)
     {
         List<FsCourseWatchLogListVO> list = fsCourseWatchLogService.selectFsCourseWatchLogListVO(param);
         ExcelUtil<FsCourseWatchLogListVO> util = new ExcelUtil<FsCourseWatchLogListVO>(FsCourseWatchLogListVO.class);
@@ -164,4 +215,15 @@ public class FsCourseWatchLogController extends BaseController
     {
         return toAjax(fsCourseWatchLogService.deleteFsCourseWatchLogByLogIds(logIds));
     }
+
+    @GetMapping("/watchLogStatistics")
+    public TableDataInfo watchLogStatistics(FsCourseOverParam param)
+    {
+        startPage();
+        if (param.getSTime()==null||param.getETime()==null){
+            return getDataTable(new ArrayList<>());
+        }
+        List<FsCourseOverVO> list = fsCourseWatchLogService.selectFsCourseWatchLogOverStatisticsListVO(param);
+        return getDataTable(list);
+    }
 }

+ 44 - 0
fs-admin/src/main/java/com/fs/course/controller/FsUserCourseCategoryController.java

@@ -3,9 +3,18 @@ package com.fs.course.controller;
 import java.util.List;
 
 import com.fs.common.core.domain.R;
+import com.fs.common.core.domain.model.LoginUser;
+import com.fs.common.utils.ServletUtils;
+import com.fs.framework.web.service.TokenService;
 import com.fs.his.domain.FsStoreProductCategory;
 import com.fs.his.vo.FsStoreProductCategoryVO;
+
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.json.JSONUtil;
+import com.fs.course.config.CourseConfig;
+
 import com.fs.his.vo.OptionsVO;
+import com.fs.system.service.ISysConfigService;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.GetMapping;
@@ -38,6 +47,12 @@ public class FsUserCourseCategoryController extends BaseController
     @Autowired
     private IFsUserCourseCategoryService fsUserCourseCategoryService;
 
+    @Autowired
+    private TokenService tokenService;
+
+    @Autowired
+    private ISysConfigService configService;
+
     /**
      * 查询课堂分类列表
      */
@@ -45,6 +60,13 @@ public class FsUserCourseCategoryController extends BaseController
     @GetMapping("/list")
     public AjaxResult list(FsUserCourseCategory fsUserCourseCategory)
     {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long userId = loginUser.getUser().getUserId();
+        String json = configService.selectConfigByKey("course.config");
+        CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
+        if (ObjectUtil.isNotEmpty(config.getIsBound())&&config.getIsBound()){
+            fsUserCourseCategory.setUserId(userId);
+        }
         List<FsUserCourseCategory> list = fsUserCourseCategoryService.selectFsUserCourseCategoryList(fsUserCourseCategory);
         return AjaxResult.success(list);
     }
@@ -57,6 +79,13 @@ public class FsUserCourseCategoryController extends BaseController
     @GetMapping("/export")
     public AjaxResult export(FsUserCourseCategory fsUserCourseCategory)
     {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long userId = loginUser.getUser().getUserId();
+        String json = configService.selectConfigByKey("course.config");
+        CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
+        if (ObjectUtil.isNotEmpty(config.getIsBound())&&config.getIsBound()){
+            fsUserCourseCategory.setUserId(userId);
+        }
         List<FsUserCourseCategory> list = fsUserCourseCategoryService.selectFsUserCourseCategoryList(fsUserCourseCategory);
         ExcelUtil<FsUserCourseCategory> util = new ExcelUtil<FsUserCourseCategory>(FsUserCourseCategory.class);
         return util.exportExcel(list, "课堂分类数据");
@@ -80,6 +109,13 @@ public class FsUserCourseCategoryController extends BaseController
     @PostMapping
     public AjaxResult add(@RequestBody FsUserCourseCategory fsUserCourseCategory)
     {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long userId = loginUser.getUser().getUserId();
+        String json = configService.selectConfigByKey("course.config");
+        CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
+        if (ObjectUtil.isNotEmpty(config.getIsBound())&&config.getIsBound()){
+            fsUserCourseCategory.setUserId(userId);
+        }
         return toAjax(fsUserCourseCategoryService.insertFsUserCourseCategory(fsUserCourseCategory));
     }
 
@@ -119,6 +155,14 @@ public class FsUserCourseCategoryController extends BaseController
     @GetMapping("/getCatePidList")
     public R getCatePidList()
     {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long userId = loginUser.getUser().getUserId();
+        String json = configService.selectConfigByKey("course.config");
+        CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
+        if (ObjectUtil.isNotEmpty(config.getIsBound())&&config.getIsBound()){
+            List<OptionsVO> list = fsUserCourseCategoryService.selectFsUserCourseCategoryPidList(userId);
+            return R.ok().put("data", list);
+        }
         List<OptionsVO> list = fsUserCourseCategoryService.selectFsUserCourseCategoryPidList();
         return R.ok().put("data", list);
     }

+ 184 - 0
fs-admin/src/main/java/com/fs/course/controller/FsUserCourseCompanyStatisticsController.java

@@ -0,0 +1,184 @@
+package com.fs.course.controller;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+
+import com.fs.common.exception.ServiceException;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.enums.BusinessType;
+import com.fs.course.domain.FsUserCourseCompanyStatistics;
+import com.fs.course.service.IFsUserCourseCompanyStatisticsService;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.common.core.page.TableDataInfo;
+
+/**
+ * 会员每日看课统计Controller
+ *
+ * @author fs
+ * @date 2025-10-27
+ */
+@RestController
+@RequestMapping("/course/statistics")
+public class FsUserCourseCompanyStatisticsController extends BaseController
+{
+    @Autowired
+    private IFsUserCourseCompanyStatisticsService fsUserCourseCompanyStatisticsService;
+
+    /**
+     * 查询会员每日看课统计列表
+     */
+    @PreAuthorize("@ss.hasPermi('course:statistics:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(FsUserCourseCompanyStatistics fsUserCourseCompanyStatistics)
+    {
+        if (fsUserCourseCompanyStatistics.getBeginTime() == null || fsUserCourseCompanyStatistics.getEndTime() == null) {
+            throw new ServiceException("请选择开始时间和结束时间!");
+        }
+
+        startPage();
+        List<FsUserCourseCompanyStatistics> list =
+                fsUserCourseCompanyStatisticsService.selectFsUserCourseCompanyStatisticsTotal(fsUserCourseCompanyStatistics);
+        Optional.ofNullable(list).orElse(Collections.emptyList())
+                .forEach(item -> {
+                    // 完播率
+                    Long watchCount = item.getWatchCount() != null ? item.getWatchCount() : 0L;
+                    Long completeWatchCount = item.getCompleteWatchCount() != null ? item.getCompleteWatchCount() : 0L;
+                    if (watchCount > 0) {
+                        BigDecimal rate = BigDecimal.valueOf(completeWatchCount)
+                                .multiply(BigDecimal.valueOf(100))
+                                .divide(BigDecimal.valueOf(watchCount), 2, RoundingMode.HALF_UP);
+                        item.setCompleteRate(rate.longValue());
+                    } else {
+                        item.setCompleteRate(0L);
+                    }
+
+                    // 正确率
+                    Long answerCount = item.getAnswerCount() != null ? item.getAnswerCount() : 0L;
+                    Long correctCount = item.getCorrectCount() != null ? item.getCorrectCount() : 0L;
+                    if (answerCount > 0) {
+                        BigDecimal rate = BigDecimal.valueOf(correctCount)
+                                .multiply(BigDecimal.valueOf(100))
+                                .divide(BigDecimal.valueOf(answerCount), 2, RoundingMode.HALF_UP);
+                        item.setCorrectRate(rate.longValue());
+                    } else {
+                        item.setCorrectRate(0L);
+                    }
+                });
+
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出会员每日看课统计列表
+     */
+    @PreAuthorize("@ss.hasPermi('course:statistics:export')")
+    @Log(title = "会员每日看课统计", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(FsUserCourseCompanyStatistics fsUserCourseCompanyStatistics) {
+
+        List<FsUserCourseCompanyStatistics> list =
+                fsUserCourseCompanyStatisticsService.selectFsUserCourseCompanyStatisticsTotal(fsUserCourseCompanyStatistics);
+
+        if (list == null) {
+            list = Collections.emptyList();
+        }
+
+        for (FsUserCourseCompanyStatistics item : list) {
+
+            Long watchCount = item.getWatchCount();
+            Long completeWatchCount = Optional.ofNullable(item.getCompleteWatchCount()).orElse(0L);
+
+            // 完播率 = 完播次数 / 观看次数 * 100  (放大100倍存入long,再格式化两位小数)
+            if (watchCount != null && watchCount > 0) {
+                Long rateValue = BigDecimal.valueOf(completeWatchCount)
+                        .multiply(BigDecimal.valueOf(10000)) // 100*100
+                        .divide(BigDecimal.valueOf(watchCount), 0, RoundingMode.HALF_UP)
+                        .longValue();
+                item.setCompleteRate(rateValue);
+                item.setCompleteRateStr(String.format("%.2f%%", rateValue / 100.0));  // Excel 格式化
+            } else {
+                item.setCompleteRate(0L);
+                item.setCompleteRateStr("0.00%");
+            }
+
+            Long answerCount = item.getAnswerCount();
+            Long correctCount = Optional.ofNullable(item.getCorrectCount()).orElse(0L);
+
+            // 正确率 = 正确人次 / 答题人次 * 100
+            if (answerCount != null && answerCount > 0) {
+                Long rateValue = BigDecimal.valueOf(correctCount)
+                        .multiply(BigDecimal.valueOf(10000))
+                        .divide(BigDecimal.valueOf(answerCount), 0, RoundingMode.HALF_UP)
+                        .longValue();
+                item.setCorrectRate(rateValue);
+                item.setCorrectRateStr(String.format("%.2f%%", rateValue / 100.0));
+            } else {
+                item.setCorrectRate(0L);
+                item.setCorrectRateStr("0.00%");
+            }
+        }
+
+        ExcelUtil<FsUserCourseCompanyStatistics> util = new ExcelUtil<>(FsUserCourseCompanyStatistics.class);
+        return util.exportExcel(list, "会员每日看课统计数据");
+    }
+
+
+
+    /**
+     * 获取会员每日看课统计详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('course:statistics:query')")
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable("id") Long id)
+    {
+        return AjaxResult.success(fsUserCourseCompanyStatisticsService.selectFsUserCourseCompanyStatisticsById(id));
+    }
+
+    /**
+     * 新增会员每日看课统计
+     */
+    @PreAuthorize("@ss.hasPermi('course:statistics:add')")
+    @Log(title = "会员每日看课统计", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody FsUserCourseCompanyStatistics fsUserCourseCompanyStatistics)
+    {
+        return toAjax(fsUserCourseCompanyStatisticsService.insertFsUserCourseCompanyStatistics(fsUserCourseCompanyStatistics));
+    }
+
+    /**
+     * 修改会员每日看课统计
+     */
+    @PreAuthorize("@ss.hasPermi('course:statistics:edit')")
+    @Log(title = "会员每日看课统计", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody FsUserCourseCompanyStatistics fsUserCourseCompanyStatistics)
+    {
+        return toAjax(fsUserCourseCompanyStatisticsService.updateFsUserCourseCompanyStatistics(fsUserCourseCompanyStatistics));
+    }
+
+    /**
+     * 删除会员每日看课统计
+     */
+    @PreAuthorize("@ss.hasPermi('course:statistics:remove')")
+    @Log(title = "会员每日看课统计", businessType = BusinessType.DELETE)
+	@DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids)
+    {
+        return toAjax(fsUserCourseCompanyStatisticsService.deleteFsUserCourseCompanyStatisticsByIds(ids));
+    }
+}

+ 94 - 72
fs-admin/src/main/java/com/fs/course/controller/FsUserCourseController.java

@@ -1,36 +1,34 @@
 package com.fs.course.controller;
 
-import java.util.List;
-
 import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.json.JSONUtil;
+import com.fs.common.annotation.Log;
+import com.fs.common.annotation.RepeatSubmit;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.domain.model.LoginUser;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
 import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.course.config.CourseConfig;
+import com.fs.course.domain.FsUserCourse;
+import com.fs.course.params.FsUserCourseConfigParam;
+import com.fs.course.service.IFsUserCourseService;
+import com.fs.course.service.IFsUserCourseVideoService;
 import com.fs.course.vo.FsUserCourseListPVO;
 import com.fs.framework.web.service.TokenService;
 import com.fs.his.utils.RedisCacheUtil;
 import com.fs.his.vo.OptionsVO;
+import com.fs.qw.param.FsUserCourseRedPageParam;
+import com.fs.sop.service.IQwSopTempService;
 import com.fs.system.service.ISysConfigService;
-import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.PutMapping;
-import org.springframework.web.bind.annotation.DeleteMapping;
-import org.springframework.web.bind.annotation.PathVariable;
-import org.springframework.web.bind.annotation.RequestBody;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
-import com.fs.common.annotation.Log;
-import com.fs.common.core.controller.BaseController;
-import com.fs.common.core.domain.AjaxResult;
-import com.fs.common.enums.BusinessType;
-import com.fs.course.domain.FsUserCourse;
-import com.fs.course.service.IFsUserCourseService;
-import com.fs.common.utils.poi.ExcelUtil;
-import com.fs.common.core.page.TableDataInfo;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
 
 /**
  * 课程Controller
@@ -40,11 +38,13 @@ import com.fs.common.core.page.TableDataInfo;
  */
 @RestController
 @RequestMapping("/course/userCourse")
-public class FsUserCourseController extends BaseController
-{
+public class FsUserCourseController extends BaseController {
     @Autowired
     private IFsUserCourseService fsUserCourseService;
 
+    @Autowired
+    private IFsUserCourseVideoService courseVideoService;
+
     @Autowired
     private RedisCacheUtil redisCacheUtil;
 
@@ -54,19 +54,21 @@ public class FsUserCourseController extends BaseController
     @Autowired
     private ISysConfigService configService;
 
+    @Autowired
+    private IQwSopTempService sopTempService;
+
     /**
      * 查询课程列表
      */
     @PreAuthorize("@ss.hasPermi('course:userCourse:list')")
     @GetMapping("/list")
-    public TableDataInfo list(FsUserCourse fsUserCourse)
-    {
+    public TableDataInfo list(FsUserCourse fsUserCourse) {
         startPage();
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         Long userId = loginUser.getUser().getUserId();
         String json = configService.selectConfigByKey("course.config");
         CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
-        if (ObjectUtil.isNotEmpty(config.getIsBound())&&config.getIsBound()){
+        if (ObjectUtil.isNotEmpty(config.getIsBound()) && config.getIsBound()) {
             fsUserCourse.setUserId(userId);
         }
         List<FsUserCourseListPVO> list = fsUserCourseService.selectFsUserCourseListPVO(fsUserCourse);
@@ -78,14 +80,13 @@ public class FsUserCourseController extends BaseController
      */
     @PreAuthorize("@ss.hasPermi('course:userCourse:publicList')")
     @GetMapping("/publicList")
-    public TableDataInfo publicList(FsUserCourse fsUserCourse)
-    {
+    public TableDataInfo publicList(FsUserCourse fsUserCourse) {
         startPage();
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         Long userId = loginUser.getUser().getUserId();
         String json = configService.selectConfigByKey("course.config");
         CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
-        if (ObjectUtil.isNotEmpty(config.getIsBound())&&config.getIsBound()){
+        if (ObjectUtil.isNotEmpty(config.getIsBound()) && config.getIsBound()) {
             fsUserCourse.setUserId(userId);
         }
         List<FsUserCourseListPVO> list = fsUserCourseService.selectFsUserCourseListPVO(fsUserCourse);
@@ -98,13 +99,12 @@ public class FsUserCourseController extends BaseController
     @PreAuthorize("@ss.hasPermi('course:userCourse:export')")
     @Log(title = "课程", businessType = BusinessType.EXPORT)
     @GetMapping("/export")
-    public AjaxResult export(FsUserCourse fsUserCourse)
-    {
+    public AjaxResult export(FsUserCourse fsUserCourse) {
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         Long userId = loginUser.getUser().getUserId();
         String json = configService.selectConfigByKey("course.config");
         CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
-        if (ObjectUtil.isNotEmpty(config.getIsBound())&&config.getIsBound()){
+        if (ObjectUtil.isNotEmpty(config.getIsBound()) && config.getIsBound()) {
             fsUserCourse.setUserId(userId);
         }
         List<FsUserCourse> list = fsUserCourseService.selectFsUserCourseList(fsUserCourse);
@@ -118,13 +118,12 @@ public class FsUserCourseController extends BaseController
     @PreAuthorize("@ss.hasPermi('course:userCourse:publicExport')")
     @Log(title = "课程", businessType = BusinessType.EXPORT)
     @GetMapping("/publicExport")
-    public AjaxResult publicExport(FsUserCourse fsUserCourse)
-    {
+    public AjaxResult publicExport(FsUserCourse fsUserCourse) {
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         Long userId = loginUser.getUser().getUserId();
         String json = configService.selectConfigByKey("course.config");
         CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
-        if (ObjectUtil.isNotEmpty(config.getIsBound())&&config.getIsBound()){
+        if (ObjectUtil.isNotEmpty(config.getIsBound()) && config.getIsBound()) {
             fsUserCourse.setUserId(userId);
         }
         List<FsUserCourse> list = fsUserCourseService.selectFsUserCourseList(fsUserCourse);
@@ -137,8 +136,7 @@ public class FsUserCourseController extends BaseController
      */
     @PreAuthorize("@ss.hasPermi('course:userCourse:query')")
     @GetMapping(value = "/{courseId}")
-    public AjaxResult getInfo(@PathVariable("courseId") Long courseId)
-    {
+    public AjaxResult getInfo(@PathVariable("courseId") Long courseId) {
         return AjaxResult.success(fsUserCourseService.selectFsUserCourseByCourseId(courseId));
     }
 
@@ -147,8 +145,7 @@ public class FsUserCourseController extends BaseController
      */
     @PreAuthorize("@ss.hasPermi('course:userCourse:publicQuery')")
     @GetMapping(value = "/public/{courseId}")
-    public AjaxResult publicGetInfo(@PathVariable("courseId") Long courseId)
-    {
+    public AjaxResult publicGetInfo(@PathVariable("courseId") Long courseId) {
         return AjaxResult.success(fsUserCourseService.selectFsUserCourseByCourseId(courseId));
     }
 
@@ -158,13 +155,12 @@ public class FsUserCourseController extends BaseController
     @PreAuthorize("@ss.hasPermi('course:userCourse:add')")
     @Log(title = "课程", businessType = BusinessType.INSERT)
     @PostMapping
-    public AjaxResult add(@RequestBody FsUserCourse fsUserCourse)
-    {
+    public AjaxResult add(@RequestBody FsUserCourse fsUserCourse) {
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         Long userId = loginUser.getUser().getUserId();
         String json = configService.selectConfigByKey("course.config");
         CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
-        if (ObjectUtil.isNotEmpty(config.getIsBound())&&config.getIsBound()){
+        if (ObjectUtil.isNotEmpty(config.getIsBound()) && config.getIsBound()) {
             fsUserCourse.setUserId(userId);
         }
         fsUserCourseService.insertFsUserCourse(fsUserCourse);
@@ -179,13 +175,12 @@ public class FsUserCourseController extends BaseController
     @PreAuthorize("@ss.hasPermi('course:userCourse:publicAdd')")
     @Log(title = "课程", businessType = BusinessType.INSERT)
     @PostMapping("/public")
-    public AjaxResult publicAdd(@RequestBody FsUserCourse fsUserCourse)
-    {
+    public AjaxResult publicAdd(@RequestBody FsUserCourse fsUserCourse) {
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         Long userId = loginUser.getUser().getUserId();
         String json = configService.selectConfigByKey("course.config");
         CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
-        if (ObjectUtil.isNotEmpty(config.getIsBound())&&config.getIsBound()){
+        if (ObjectUtil.isNotEmpty(config.getIsBound()) && config.getIsBound()) {
             fsUserCourse.setUserId(userId);
         }
         fsUserCourseService.insertFsUserCourse(fsUserCourse);
@@ -200,21 +195,30 @@ public class FsUserCourseController extends BaseController
     @PreAuthorize("@ss.hasPermi('course:userCourse:edit')")
     @Log(title = "课程", businessType = BusinessType.UPDATE)
     @PutMapping
-    public AjaxResult edit(@RequestBody FsUserCourse fsUserCourse)
-    {
+    public AjaxResult edit(@RequestBody FsUserCourse fsUserCourse) {
         fsUserCourseService.updateFsUserCourse(fsUserCourse);
         redisCacheUtil.delRedisKey("getCourseList");
         return toAjax(1);
     }
 
+    /**
+     * 统一修改课程红包
+     */
+    @PreAuthorize("@ss.hasPermi('course:userCourse:editRedPage')")
+    @Log(title = "修改课程红包", businessType = BusinessType.UPDATE)
+    @PostMapping("/editRedPage")
+    public AjaxResult editRedPage(@RequestBody FsUserCourseRedPageParam redPageParam) {
+        courseVideoService.updateFsUserCourseRedPage(redPageParam);
+        return toAjax(1);
+    }
+
     /**
      * 修改课程
      */
     @PreAuthorize("@ss.hasPermi('course:userCourse:publicEdit')")
     @Log(title = "课程", businessType = BusinessType.UPDATE)
     @PutMapping("/public")
-    public AjaxResult publicEdit(@RequestBody FsUserCourse fsUserCourse)
-    {
+    public AjaxResult publicEdit(@RequestBody FsUserCourse fsUserCourse) {
         fsUserCourseService.updateFsUserCourse(fsUserCourse);
         redisCacheUtil.delRedisKey("getCourseList");
         return toAjax(1);
@@ -226,8 +230,7 @@ public class FsUserCourseController extends BaseController
     @PreAuthorize("@ss.hasPermi('course:userCourse:copy')")
     @Log(title = "课程", businessType = BusinessType.DELETE)
     @GetMapping("/copy/{courseId}")
-    public AjaxResult copy(@PathVariable Long courseId)
-    {
+    public AjaxResult copy(@PathVariable Long courseId) {
         int i = fsUserCourseService.copyFsUserCourse(courseId);
         return toAjax(i);
     }
@@ -237,8 +240,8 @@ public class FsUserCourseController extends BaseController
      */
     @PreAuthorize("@ss.hasPermi('course:userCourse:remove')")
     @Log(title = "课程", businessType = BusinessType.DELETE)
-	@DeleteMapping("/{courseIds}")
-    public AjaxResult remove(@PathVariable Long[] courseIds){
+    @DeleteMapping("/{courseIds}")
+    public AjaxResult remove(@PathVariable Long[] courseIds) {
         fsUserCourseService.deleteFsUserCourseByCourseIds(courseIds);
         redisCacheUtil.delRedisKey("getCourseList");
         return toAjax(1);
@@ -250,8 +253,7 @@ public class FsUserCourseController extends BaseController
     @PreAuthorize("@ss.hasPermi('course:userCourse:publicRemove')")
     @Log(title = "课程", businessType = BusinessType.DELETE)
     @DeleteMapping("/public/{courseIds}")
-    public AjaxResult publicRemove(@PathVariable Long[] courseIds)
-    {
+    public AjaxResult publicRemove(@PathVariable Long[] courseIds) {
         fsUserCourseService.deleteFsUserCourseByCourseIds(courseIds);
         redisCacheUtil.delRedisKey("getCourseList");
         return toAjax(1);
@@ -259,8 +261,7 @@ public class FsUserCourseController extends BaseController
 
 
     @GetMapping("/getAllList")
-    public R getAllList()
-    {
+    public R getAllList() {
         List<OptionsVO> list = fsUserCourseService.selectFsUserCourseAllList();
         return R.ok().put("data", list);
     }
@@ -268,18 +269,16 @@ public class FsUserCourseController extends BaseController
     @PreAuthorize("@ss.hasPermi('course:userCourse:updateIsShow')")
     @Log(title = "课程上架", businessType = BusinessType.UPDATE)
     @PostMapping("/updateIsShow")
-    public AjaxResult updateIsShow(@RequestBody FsUserCourse fsUserCourse)
-    {
+    public AjaxResult updateIsShow(@RequestBody FsUserCourse fsUserCourse) {
         fsUserCourseService.updateFsUserCourse(fsUserCourse);
         redisCacheUtil.delRedisKey("getCourseList");
-       return toAjax(1);
+        return toAjax(1);
     }
 
     @PreAuthorize("@ss.hasPermi('course:userCourse:publicUpdateIsShow')")
     @Log(title = "课程上架", businessType = BusinessType.UPDATE)
     @PostMapping("/publicUpdateIsShow")
-    public AjaxResult publicUpdateIsShow(@RequestBody FsUserCourse fsUserCourse)
-    {
+    public AjaxResult publicUpdateIsShow(@RequestBody FsUserCourse fsUserCourse) {
         fsUserCourseService.updateFsUserCourse(fsUserCourse);
         redisCacheUtil.delRedisKey("getCourseList");
         return toAjax(1);
@@ -288,9 +287,8 @@ public class FsUserCourseController extends BaseController
     @PreAuthorize("@ss.hasPermi('course:userCourse:putOn')")
     @Log(title = "课程批量上架", businessType = BusinessType.UPDATE)
     @PostMapping("/putOn/{courseIds}")
-    public AjaxResult putOn(@PathVariable Long[] courseIds)
-    {
-        fsUserCourseService.updateFsUserCourseIsShow(courseIds,1);
+    public AjaxResult putOn(@PathVariable Long[] courseIds) {
+        fsUserCourseService.updateFsUserCourseIsShow(courseIds, 1);
         redisCacheUtil.delRedisKey("getCourseList");
         return toAjax(1);
     }
@@ -298,9 +296,8 @@ public class FsUserCourseController extends BaseController
     @PreAuthorize("@ss.hasPermi('course:userCourse:publicPutOn')")
     @Log(title = "课程批量上架", businessType = BusinessType.UPDATE)
     @PostMapping("/publicPutOn/{courseIds}")
-    public AjaxResult publicPutOn(@PathVariable Long[] courseIds)
-    {
-        fsUserCourseService.updateFsUserCourseIsShow(courseIds,1);
+    public AjaxResult publicPutOn(@PathVariable Long[] courseIds) {
+        fsUserCourseService.updateFsUserCourseIsShow(courseIds, 1);
         redisCacheUtil.delRedisKey("getCourseList");
         return toAjax(1);
     }
@@ -308,9 +305,8 @@ public class FsUserCourseController extends BaseController
     @PreAuthorize("@ss.hasPermi('course:userCourse:putOn')")
     @Log(title = "课程批量下架", businessType = BusinessType.UPDATE)
     @PostMapping("/pullOff/{courseIds}")
-    public AjaxResult pullOff(@PathVariable Long[] courseIds)
-    {
-        fsUserCourseService.updateFsUserCourseIsShow(courseIds,0);
+    public AjaxResult pullOff(@PathVariable Long[] courseIds) {
+        fsUserCourseService.updateFsUserCourseIsShow(courseIds, 0);
         redisCacheUtil.delRedisKey("getCourseList");
         return toAjax(1);
     }
@@ -318,10 +314,36 @@ public class FsUserCourseController extends BaseController
     @PreAuthorize("@ss.hasPermi('course:userCourse:publicPutOff')")
     @Log(title = "课程批量下架", businessType = BusinessType.UPDATE)
     @PostMapping("/publicPutOff/{courseIds}")
-    public AjaxResult publicPutOff(@PathVariable Long[] courseIds)
-    {
-        fsUserCourseService.updateFsUserCourseIsShow(courseIds,0);
+    public AjaxResult publicPutOff(@PathVariable Long[] courseIds) {
+        fsUserCourseService.updateFsUserCourseIsShow(courseIds, 0);
+        redisCacheUtil.delRedisKey("getCourseList");
+        return toAjax(1);
+    }
+
+    /**
+     * 修改配置
+     **/
+    @PreAuthorize("@ss.hasPermi('course:userCourse:editConfig')")
+    @Log(title = "课程配置", businessType = BusinessType.UPDATE)
+    @PostMapping("/editConfig")
+    public R editConfig(@RequestBody FsUserCourseConfigParam params) {
+        fsUserCourseService.editConfig(params.getId(), params.getConfigJson());
         redisCacheUtil.delRedisKey("getCourseList");
+        redisCacheUtil.delRedisKey("h5user:course:video:list:all");
+        redisCacheUtil.delRedisKey("h5user:course:list:all");
+        redisCacheUtil.delRedisKey("cache:video");
+        return R.ok();
+    }
+
+    /**
+     * 同步课程模板
+     */
+    @PreAuthorize("@ss.hasPermi('course:userCourseVideo:sync')")
+    @Log(title = "同步课程模板", businessType = BusinessType.UPDATE)
+    @RepeatSubmit
+    @PostMapping("/syncTemplate/{courseId}")
+    public AjaxResult syncTemplate(@PathVariable Long courseId) {
+        sopTempService.syncTemplate(courseId);
         return toAjax(1);
     }
 }

+ 45 - 0
fs-admin/src/main/java/com/fs/course/controller/FsUserCoursePeriodController.java

@@ -1,5 +1,6 @@
 package com.fs.course.controller;
 
+import com.fs.baidu.domain.BdAccount;
 import com.fs.common.annotation.Log;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.AjaxResult;
@@ -15,6 +16,7 @@ import com.fs.course.service.IFsUserCoursePeriodDaysService;
 import com.fs.course.service.IFsUserCoursePeriodService;
 import com.fs.course.service.IFsUserCourseVideoRedPackageService;
 import com.fs.course.vo.*;
+import com.fs.course.vo.newfs.FsCourseAnalysisCountVO;
 import com.fs.his.vo.OptionsVO;
 import com.github.pagehelper.PageHelper;
 import com.github.pagehelper.PageInfo;
@@ -27,6 +29,7 @@ import org.springframework.web.bind.annotation.*;
 
 import java.math.BigDecimal;
 import java.time.LocalDateTime;
+import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -161,14 +164,17 @@ public class FsUserCoursePeriodController extends BaseController {
 
     @PreAuthorize("@ss.hasPermi('course:period:updateCourseTime')")
     @PostMapping("/updateCourseTime")
+    @Log(title = "会员营期修改课程时间", businessType = BusinessType.UPDATE)
     public R updateCourseTime(@RequestBody UpdateCourseTimeVo vo){
         return fsUserCoursePeriodDaysService.updateCourseTime(vo);
     }
     @PostMapping("/updateCourseDate")
+    @Log(title = "会员营期修改CourseDate", businessType = BusinessType.UPDATE)
     public R updateCourseDate(@RequestBody UpdateCourseTimeVo vo){
         return fsUserCoursePeriodDaysService.updateCourseDate(vo);
     }
     @PostMapping("/updateListCourseData")
+    @Log(title = "会员营期修改ListCourseData", businessType = BusinessType.UPDATE)
     public R updateListCourseData(@RequestBody List<FsUserCoursePeriodDays> entity){
         return fsUserCoursePeriodDaysService.updateListCourseData(entity);
     }
@@ -228,6 +234,44 @@ public class FsUserCoursePeriodController extends BaseController {
         return R.ok(result);
     }
 
+    /**
+     * @Description: 会员营期导出课程统计
+     * @Param:
+     * @Return:
+     * @Author xgb
+     * @Date 2025/12/12 11:37
+     */
+    @PostMapping("/exportInfo")
+    @ApiOperation("营期统计")
+    public AjaxResult exportInfo(@RequestBody PeriodCountParam param) {
+        List<FsPeriodCountExportVO> exportList = new ArrayList<>();
+        PageHelper.startPage(param.getPageNum(), param.getPageSize());
+        List<FsPeriodCountVO> list = fsUserCoursePeriodDaysService.periodCourseCount(param);
+        list.forEach(item -> {
+            FsPeriodCountExportVO exportVO = new FsPeriodCountExportVO();
+            exportVO.setTitle(item.getTitle());
+            exportVO.setDayDate(item.getDayDate());
+            if(item.getCountDetailsVO() != null){
+                FsCourseAnalysisCountVO countDetailsVO =item.getCountDetailsVO();
+                exportVO.setCourseWatchTimes(countDetailsVO.getCourseWatchTimes());
+                exportVO.setCourseCompleteTimes(countDetailsVO.getCourseCompleteTimes());
+                exportVO.setCourseWatchNum(countDetailsVO.getCourseWatchNum());
+                exportVO.setCourseCompleteNum(countDetailsVO.getCourseCompleteNum());
+                exportVO.setCompleteRate(countDetailsVO.getCompleteRate()+"%");
+                exportVO.setAnswerTimes(countDetailsVO.getAnswerTimes());
+                exportVO.setAnswerNum(countDetailsVO.getAnswerNum());
+                exportVO.setAnswerRightNum(countDetailsVO.getAnswerRightNum());
+                exportVO.setAnswerRightRate(countDetailsVO.getAnswerRightRate()+"%");
+                exportVO.setRedPacketNum(countDetailsVO.getRedPacketNum());
+                exportVO.setRedPacketAmount(countDetailsVO.getRedPacketAmount());
+            }
+            exportList.add(exportVO);
+        });
+
+        ExcelUtil<FsPeriodCountExportVO> util = new ExcelUtil<FsPeriodCountExportVO>(FsPeriodCountExportVO.class);
+        return util.exportExcel(exportList, "百度账号数据");
+    }
+
     @GetMapping("/getPeriodListLikeName")
     public R getPeriodListLikeName(@RequestParam(required = false) String name,
                                    @RequestParam(required = false) Long campId,
@@ -275,6 +319,7 @@ public class FsUserCoursePeriodController extends BaseController {
 
     @PreAuthorize("@ss.hasPermi('course:period:setCompanyRedPacket')")
     @ApiOperation("按公司批量保存设置红包金额")
+    @Log(title = "按公司批量保存设置红包金额", businessType = BusinessType.UPDATE)
     @PostMapping("/batchRedPacket/byCompany")
     public R batchRedPacketByCompany(@Validated @RequestBody BatchCompanyRedPackageParam param) {
         try {

+ 41 - 0
fs-admin/src/main/java/com/fs/course/controller/FsUserCourseVideoController.java

@@ -20,8 +20,10 @@ import com.fs.course.param.BatchVideoSvae;
 import com.fs.course.param.CourseVideoUpdates;
 import com.fs.course.service.IFsUserCourseService;
 import com.fs.course.service.IFsUserCourseVideoService;
+import com.fs.course.vo.FsUserCourseVideoChooseVO;
 import com.fs.framework.web.service.TokenService;
 import com.fs.his.vo.OptionsVO;
+import com.fs.qw.vo.SortDayVo;
 import com.fs.system.service.ISysConfigService;
 import com.github.pagehelper.PageHelper;
 import com.github.pagehelper.PageInfo;
@@ -175,6 +177,24 @@ public class FsUserCourseVideoController extends BaseController
         return getDataTable(list);
     }
 
+    @GetMapping("/getVideoListByCourseIdAll")
+    public TableDataInfo getVideoListByCourseIdAll(Long courseId)
+    {
+
+        FsUserCourseVideo courseVideo=new FsUserCourseVideo();
+        courseVideo.setCourseId(courseId);
+        courseVideo.setIsDel(0);
+        List<FsUserCourseVideo> list = fsUserCourseVideoService.selectFsUserCourseVideoList(courseVideo);
+        return getDataTable(list);
+    }
+
+    @PostMapping("/sortCourseVideo")
+    public AjaxResult sortCourseVideo(@RequestBody List<FsUserCourseVideo> list){
+        fsUserCourseVideoService.sortCourseVideo(list);
+        return toAjax(1);
+    }
+
+
     @GetMapping("/getSort/{courseId}")
     public R remove(@PathVariable("courseId") Long courseId)
     {
@@ -202,6 +222,7 @@ public class FsUserCourseVideoController extends BaseController
         return R.ok();
     }
     @PostMapping("/batchUpdateRed")
+    @Log(title = "按课程批量保存设置红包金额", businessType = BusinessType.UPDATE)
     public R batchUpdateRed(@RequestBody List<BatchRedUpdate> list){
         fsUserCourseVideoService.batchUpdateRed(list);
         return R.ok();
@@ -221,4 +242,24 @@ public class FsUserCourseVideoController extends BaseController
         List<OptionsVO> periodList = fsUserCourseVideoService.selectVideoListByMap(params);
         return R.ok().put("data", new PageInfo<>(periodList));
     }
+
+    @GetMapping("/getChooseCourseVideoList")
+    public R getChooseCourseVideoList(@RequestParam(required = false) Long courseId,
+                                      @RequestParam(required = false, defaultValue = "1") Integer pageNum,
+                                      @RequestParam(required = false, defaultValue = "10") Integer pageSize) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long userId = loginUser.getUser().getUserId();
+        String json = configService.selectConfigByKey("course.config");
+        CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
+
+        Map<String,Object> params = new HashMap<>();
+        params.put("courseId", courseId);
+        if (ObjectUtil.isNotEmpty(config.getIsBound())&&config.getIsBound()){
+            params.put("userId", userId);
+        }
+
+        PageHelper.startPage(pageNum, pageSize);
+        List<FsUserCourseVideoChooseVO> list = fsUserCourseVideoService.getChooseCourseVideoListByMap(params);
+        return R.ok().put("data", new PageInfo<>(list));
+    }
 }

+ 65 - 5
fs-admin/src/main/java/com/fs/course/controller/FsUserVideoController.java

@@ -13,6 +13,7 @@ import com.fs.course.domain.FsUserCourseVideo;
 import com.fs.course.domain.FsUserVideo;
 import com.fs.course.domain.FsVideoResource;
 import com.fs.course.param.*;
+import com.fs.course.service.HsyAssumeRoleService;
 import com.fs.course.service.IFsUserCourseVideoService;
 import com.fs.course.service.IFsUserVideoService;
 import com.fs.course.service.IFsVideoResourceService;
@@ -23,17 +24,18 @@ import com.fs.his.service.IFsPackageService;
 import com.fs.his.vo.FsPackageVO;
 import com.fs.system.oss.CloudStorageService;
 import com.fs.system.oss.OSSFactory;
+import com.volcengine.model.response.AssumeRoleResponse;
+import com.volcengine.service.vod.IVodService;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.io.FilenameUtils;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
 import org.springframework.web.multipart.MultipartFile;
 
 import java.io.*;
-import java.util.Date;
-import java.util.List;
-import java.util.UUID;
+import java.util.*;
 
 /**
  * 课堂视频Controller
@@ -60,6 +62,12 @@ public class FsUserVideoController extends BaseController
     @Autowired
     private IFsUserCourseVideoService fsUserCourseVideoService;
 
+
+
+    @Autowired
+    private RedisTemplate redisTemplate;
+    @Autowired
+    private HsyAssumeRoleService hsyAssumeRoleService;
     /**
      * 查询课堂视频列表
      */
@@ -180,8 +188,11 @@ public class FsUserVideoController extends BaseController
         return toAjax(fsUserVideoService.updateFsUserVideoIsShow(videoIds,0));
     }
 
-    private static final String VIDEO_UPLOAD_DIR = "C:\\fs\\uploadPath\\userVideo\\video";  // 上传目录
-    private static final String FRAME_OUTPUT_DIR = "C:\\fs\\uploadPath\\userVideo\\frame";  // 输出帧的目录
+//    private static final String VIDEO_UPLOAD_DIR = "C:\\fs\\uploadPath\\userVideo\\video";  // 上传目录
+//    private static final String FRAME_OUTPUT_DIR = "C:\\fs\\uploadPath\\userVideo\\frame";  // 输出帧的目录
+    // 改为使用系统临时目录或相对路径
+    private static final String VIDEO_UPLOAD_DIR = System.getProperty("java.io.tmpdir") + File.separator + "fs_upload" + File.separator + "userVideo" + File.separator + "video";
+    private static final String FRAME_OUTPUT_DIR = System.getProperty("java.io.tmpdir") + File.separator + "fs_upload" + File.separator + "userVideo" + File.separator + "frame";
 
 
     /**
@@ -198,16 +209,19 @@ public class FsUserVideoController extends BaseController
 
         // 保存上传的视频文件
         String videoFileName = System.currentTimeMillis() + "_" + UUID.randomUUID().toString().replaceAll("-", "").substring(0, 16);
+        createDir(VIDEO_UPLOAD_DIR);
         File videoFile = new File(VIDEO_UPLOAD_DIR, videoFileName);
         try {
             file.transferTo(videoFile);
         } catch (IOException e) {
+            e.printStackTrace();
             // 记录错误日志
             return R.error("获取封面失败");
         }
 
         // 提取视频第一帧
         String frameFileName = FilenameUtils.removeExtension(videoFileName) + "_frame.jpg";
+        createDir(FRAME_OUTPUT_DIR);
         File frameFile = new File(FRAME_OUTPUT_DIR, frameFileName);
         try {
             extractFirstFrame(videoFile.getAbsolutePath(), frameFile.getAbsolutePath());
@@ -268,6 +282,17 @@ public class FsUserVideoController extends BaseController
         }
     }
 
+    private void createDir(String path){
+        File videoUploadDir = new File(path);
+        if (!videoUploadDir.exists()) {
+            boolean created = videoUploadDir.mkdirs();
+            if (!created) {
+                log.error("创建视频上传目录失败: {}", path);
+            }
+            log.info("创建视频上传目录: {}", path);
+        }
+    }
+
     @PostMapping("/updateUrl")
     public R updateUrl()
     {
@@ -372,4 +397,39 @@ public class FsUserVideoController extends BaseController
             return R.error("服务器内部错误");
         }
     }
+
+
+    @PreAuthorize("@ss.hasPermi('course:userVideo:uploadUserVideo')")
+    @Log(title = "课堂视频", businessType = BusinessType.INSERT)
+    @PostMapping("/uploadUserVideo")
+    public AjaxResult upload(@RequestParam("file") MultipartFile file, @RequestParam("uploadId")String uploadId) {
+        String vid = fsUserVideoService.uploadVideo(file, uploadId);
+
+        Map<String, Object> res = new HashMap<>();
+        res.put("uploadId", uploadId);
+        res.put("vid", vid);
+        return new AjaxResult(200, "上传成功", res);
+    }
+
+
+    @GetMapping("/uploadProgress")
+    public AjaxResult getUploadProgress(@RequestParam String uploadId) {
+        Object percentObj = redisTemplate.opsForValue()
+                .get("vod:upload:" + uploadId);
+
+        int percent = 0;
+        if (percentObj != null) {
+            percent = Integer.parseInt(percentObj.toString());
+        }
+
+        Map<String, Object> res = new HashMap<>();
+        res.put("percent", percent);
+        return AjaxResult.success(res);
+    }
+
+    @GetMapping("/HsyAssumeRoleService")
+    public AjaxResult HsyAssumeRoleService() throws Exception {
+        AssumeRoleResponse roleResponse = hsyAssumeRoleService.getRoleResponse();
+        return AjaxResult.success(roleResponse);
+    }
 }

+ 59 - 23
fs-admin/src/main/java/com/fs/course/controller/FsVideoResourceController.java

@@ -3,6 +3,7 @@ package com.fs.course.controller;
 import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.json.JSONUtil;
 import com.baomidou.mybatisplus.core.conditions.Wrapper;
+import com.baomidou.mybatisplus.core.toolkit.StringUtils;
 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.fs.common.annotation.Log;
 import com.fs.common.core.controller.BaseController;
@@ -11,14 +12,19 @@ import com.fs.common.core.domain.model.LoginUser;
 import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
 import com.fs.common.utils.ServletUtils;
+import com.fs.config.cloud.CloudHostProper;
+import com.fs.course.business.FsVideoResourceBusinessService;
 import com.fs.course.config.CourseConfig;
 import com.fs.course.domain.FsVideoResource;
+import com.fs.course.service.IFsUserCourseVideoService;
+import com.fs.course.service.IFsUserVideoService;
 import com.fs.course.service.IFsVideoResourceService;
 import com.fs.course.vo.FsVideoResourceVO;
 import com.fs.framework.web.service.TokenService;
 import com.fs.system.service.ISysConfigService;
 import com.github.pagehelper.PageHelper;
 import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
@@ -30,6 +36,7 @@ import java.util.stream.Collectors;
 /**
  * 资源库管理
  */
+@Slf4j
 @RestController
 @RequestMapping("/course/videoResource")
 @AllArgsConstructor
@@ -37,11 +44,17 @@ public class FsVideoResourceController extends BaseController {
 
     private final IFsVideoResourceService fsVideoResourceService;
 
-    @Autowired
-    private TokenService tokenService;
+    private final TokenService tokenService;
+
+    private final ISysConfigService configService;
+
+    private final CloudHostProper cloudHostProper;
 
+    private final FsVideoResourceBusinessService videoResourceBusinessService;
     @Autowired
-    private ISysConfigService configService;
+    private IFsUserVideoService fsUserVideoService;
+    @Autowired
+    private IFsUserCourseVideoService fsUserCourseVideoService;
 
     /**
      * 查询视频素材库列表
@@ -53,8 +66,7 @@ public class FsVideoResourceController extends BaseController {
                               @RequestParam(required = false) Integer typeId,
                               @RequestParam(required = false) Integer typeSubId,
                               @RequestParam(required = false, defaultValue = "1") Integer pageNum,
-                              @RequestParam(required = false, defaultValue = "10") Integer pageSize)
-    {
+                              @RequestParam(required = false, defaultValue = "10") Integer pageSize) {
         Map<String, Object> params = new HashMap<>();
         params.put("resourceName", resourceName);
         params.put("fileName", fileName);
@@ -63,7 +75,7 @@ public class FsVideoResourceController extends BaseController {
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         String json = configService.selectConfigByKey("course.config");
         CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
-        if (ObjectUtil.isNotEmpty(config.getIsBound())&&config.getIsBound()){
+        if (ObjectUtil.isNotEmpty(config.getIsBound()) && config.getIsBound()) {
             params.put("userId", loginUser.getUser().getUserId());
         }
         PageHelper.startPage(pageNum, pageSize);
@@ -77,8 +89,7 @@ public class FsVideoResourceController extends BaseController {
      */
     @PreAuthorize("@ss.hasPermi('course:videoResource:query')")
     @GetMapping(value = "/{id}")
-    public AjaxResult getInfo(@PathVariable("id") Long id)
-    {
+    public AjaxResult getInfo(@PathVariable("id") Long id) {
         return AjaxResult.success(fsVideoResourceService.getById(id));
     }
 
@@ -88,17 +99,25 @@ public class FsVideoResourceController extends BaseController {
     @PreAuthorize("@ss.hasPermi('course:videoResource:add')")
     @Log(title = "视频素材库", businessType = BusinessType.INSERT)
     @PostMapping
-    public AjaxResult add(@RequestBody FsVideoResource fsVideoResource)
-    {
+    public AjaxResult add(@RequestBody FsVideoResource fsVideoResource) {
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         Long userId = loginUser.getUser().getUserId();
         String json = configService.selectConfigByKey("course.config");
         CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
-        if (ObjectUtil.isNotEmpty(config.getIsBound())&&config.getIsBound()){
+        if (ObjectUtil.isNotEmpty(config.getIsBound()) && config.getIsBound()) {
             fsVideoResource.setUserId(userId);
         }
+
         fsVideoResource.setCreateTime(LocalDateTime.now());
-        fsVideoResourceService.save(fsVideoResource);
+        boolean save = fsVideoResourceService.save(fsVideoResource);
+        if (save&&StringUtils.isNotEmpty(fsVideoResource.getHsyVid())){
+            try {
+                fsUserCourseVideoService.updateMediaPublishStatus(fsVideoResource.getHsyVid());
+                log.info("更新视频发布状态成功,hsyVid: {}", fsVideoResource.getHsyVid());
+            } catch (Exception e) {
+                log.error("更新视频发布状态失败,hsyVid: {}, 错误: {}", fsVideoResource.getHsyVid(), e.getMessage());
+            }
+        }
         return AjaxResult.success();
     }
 
@@ -108,8 +127,12 @@ public class FsVideoResourceController extends BaseController {
     @PreAuthorize("@ss.hasPermi('course:videoResource:edit')")
     @Log(title = "视频素材库", businessType = BusinessType.UPDATE)
     @PutMapping
-    public AjaxResult edit(@RequestBody FsVideoResource fsVideoResource)
-    {
+    public AjaxResult edit(@RequestBody FsVideoResource fsVideoResource) {
+        if (("今正科技".equals(cloudHostProper.getCompanyName()))) {
+            // 同步资源到课程
+            videoResourceBusinessService.edit(fsVideoResource);
+            return AjaxResult.success();
+        }
         fsVideoResourceService.updateById(fsVideoResource);
         return AjaxResult.success();
     }
@@ -120,8 +143,7 @@ public class FsVideoResourceController extends BaseController {
     @PreAuthorize("@ss.hasPermi('course:videoResource:remove')")
     @Log(title = "视频素材库", businessType = BusinessType.DELETE)
     @DeleteMapping("/{ids}")
-    public AjaxResult remove(@PathVariable Long[] ids)
-    {
+    public AjaxResult remove(@PathVariable Long[] ids) {
         Wrapper<FsVideoResource> updateWrapper = Wrappers.<FsVideoResource>lambdaUpdate()
                 .set(FsVideoResource::getIsDel, 1)
                 .in(FsVideoResource::getId, Arrays.asList(ids));
@@ -131,6 +153,7 @@ public class FsVideoResourceController extends BaseController {
 
     /**
      * 批量修改视频分类
+     *
      * @param typeId
      * @param typeSubId
      * @param ids
@@ -142,13 +165,13 @@ public class FsVideoResourceController extends BaseController {
     public AjaxResult batchUpdateClass(@RequestParam("typeId") Long typeId,
                                        @RequestParam("typeSubId") Long typeSubId,
                                        @RequestParam("ids") String ids) {
-        if(typeId == null || typeId <= 0){
+        if (typeId == null || typeId <= 0) {
             return AjaxResult.error("请选择分类");
         }
-        if(typeSubId == null || typeSubId <= 0){
+        if (typeSubId == null || typeSubId <= 0) {
             return AjaxResult.error("请选择子分类");
         }
-        if(ids == null || ids.isEmpty()){
+        if (ids == null || ids.isEmpty()) {
             return AjaxResult.error("请选择要修改的分类");
         }
 
@@ -178,13 +201,26 @@ public class FsVideoResourceController extends BaseController {
         String json = configService.selectConfigByKey("course.config");
         CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
 
-        list.forEach(v ->{
+        list.forEach(v -> {
             v.setCreateTime(LocalDateTime.now());
-            if (ObjectUtil.isNotEmpty(config.getIsBound())&&config.getIsBound()){
+            if (ObjectUtil.isNotEmpty(config.getIsBound()) && config.getIsBound()) {
                 v.setUserId(userId);
             }
-        } );
-        fsVideoResourceService.saveBatch(list);
+        });
+        boolean saveStatus = fsVideoResourceService.saveBatch(list);
+        if (saveStatus) {
+            list.forEach(fsVideoResource -> {
+                // 检查hsyVid是否存在且不为空
+                if (ObjectUtil.isNotEmpty(fsVideoResource.getHsyVid())) {
+                    try {
+                        fsUserCourseVideoService.updateMediaPublishStatus(fsVideoResource.getHsyVid());
+                        log.info("更新视频发布状态成功,hsyVid: {}", fsVideoResource.getHsyVid());
+                    } catch (Exception e) {
+                        log.error("更新视频发布状态失败,hsyVid: {}, 错误: {}", fsVideoResource.getHsyVid(), e.getMessage());
+                    }
+                }
+            });
+        }
         return AjaxResult.success();
     }
 }

+ 67 - 0
fs-admin/src/main/java/com/fs/course/controller/qw/QwFsCourseWatchLogController.java

@@ -3,21 +3,27 @@ package com.fs.course.controller.qw;
 import com.fs.common.annotation.Log;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.domain.R;
 import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
 import com.fs.common.exception.CustomException;
+import com.fs.common.utils.ServletUtils;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.course.domain.FsCourseWatchLog;
+import com.fs.course.param.FsCourseOverParam;
 import com.fs.course.param.FsCourseWatchLogListParam;
 import com.fs.course.param.FsCourseWatchLogStatisticsListParam;
 import com.fs.course.param.PeriodStatisticCountParam;
 import com.fs.course.service.IFsCourseWatchLogService;
+import com.fs.course.vo.FsCourseOverVO;
 import com.fs.course.vo.FsCourseWatchLogListVO;
+import com.fs.course.vo.FsCourseWatchLogStatisticsListByCompanyVO;
 import com.fs.course.vo.FsCourseWatchLogStatisticsListVO;
 import com.fs.qw.param.QwWatchLogStatisticsListParam;
 import com.fs.qw.service.IQwWatchLogService;
 import com.fs.qw.vo.QwWatchLogAllStatisticsListVO;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
 
@@ -34,6 +40,9 @@ import java.util.List;
 @RequestMapping("/qw/course/courseWatchLog")
 public class QwFsCourseWatchLogController extends BaseController
 {
+    @Value("${cloud_host.company_name}")
+    private String signProjectName;
+
     @Autowired
     private IFsCourseWatchLogService fsCourseWatchLogService;
 
@@ -73,8 +82,55 @@ public class QwFsCourseWatchLogController extends BaseController
         }
         param.setSendType(2); //企微
         List<FsCourseWatchLogStatisticsListVO> list = fsCourseWatchLogService.selectFsCourseWatchLogStatisticsListVO(param);
+        if("济南联志健康".equals(signProjectName)){
+            FsCourseWatchLogStatisticsListVO totalData = fsCourseWatchLogService.getTotalDataAddItem(param);
+            list.add(totalData);
+        }
         return getDataTable(list);
     }
+
+    /**
+     * 会员看课统计导出
+     */
+    @PreAuthorize("@ss.hasPermi('course:courseWatchLog:statisticsExport')")
+    @Log(title = "企微看课统计导出", businessType = BusinessType.EXPORT)
+    @PostMapping("/statisticsExport")
+    public AjaxResult statisticsExport(@RequestBody FsCourseWatchLogStatisticsListParam param)
+    {
+
+        if (param.getSTime()==null||param.getETime()==null){
+            throw new CustomException("必须选择开始时间和结束时间!");
+        }
+
+        param.setSendType(2); //企微
+        List<FsCourseWatchLogStatisticsListVO> list = fsCourseWatchLogService.selectFsCourseWatchLogStatisticsListVO(param);
+        if("济南联志健康".equals(signProjectName)){
+            FsCourseWatchLogStatisticsListVO totalData = fsCourseWatchLogService.getTotalDataAddItem(param);
+            list.add(totalData);
+        }
+
+        ExcelUtil<FsCourseWatchLogStatisticsListVO> util = new ExcelUtil<FsCourseWatchLogStatisticsListVO>(FsCourseWatchLogStatisticsListVO.class);
+
+        return util.exportExcel(list, "企微看课统计");
+    }
+
+
+
+    @GetMapping("/getSignProjectName")
+    public R getSignProjectName(){
+        return R.ok().put("signProjectName", signProjectName);
+    }
+
+    @GetMapping("/statisticsListByCompany")
+    public R statisticsListByCompany(FsCourseWatchLogStatisticsListParam param)
+    {
+        if (param.getSTime()==null||param.getETime()==null){
+            return R.ok().put("rows", new ArrayList<>());
+        }
+        param.setSendType(2); //企微
+        List<FsCourseWatchLogStatisticsListByCompanyVO> list = fsCourseWatchLogService.selectFsCourseWatchLogStatisticsListByCompanyVO(param);
+        return R.ok().put("rows", list);
+    }
     @GetMapping("/qwWatchLogStatisticsList")
     public TableDataInfo qwWatchLogStatisticsList(QwWatchLogStatisticsListParam param)
     {
@@ -150,4 +206,15 @@ public class QwFsCourseWatchLogController extends BaseController
         List<FsCourseWatchLogListVO> list = fsCourseWatchLogService.selectListBytrainingCampId(param);
         return getDataTable(list);
     }
+
+    @GetMapping("/watchLogStatistics")
+    public TableDataInfo watchLogStatistics(FsCourseOverParam param)
+    {
+        startPage();
+        if (param.getSTime()==null||param.getETime()==null){
+            return getDataTable(new ArrayList<>());
+        }
+        List<FsCourseOverVO> list = fsCourseWatchLogService.selectFsCourseWatchLogOverStatisticsListVO(param);
+        return getDataTable(list);
+    }
 }

+ 21 - 0
fs-admin/src/main/java/com/fs/course/params/FsUserCourseConfigParam.java

@@ -0,0 +1,21 @@
+package com.fs.course.params;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+
+@ApiModel("过程页配置参数实体")
+@Data
+public class FsUserCourseConfigParam {
+
+    @NotNull(message = "课程ID不能为空")
+    @ApiModelProperty("课程ID")
+    private Long id;
+
+    @NotBlank(message = "配置信息不能为空")
+    @ApiModelProperty("配置信息")
+    private String configJson;
+}

+ 17 - 0
fs-admin/src/main/java/com/fs/course/task/CourseStatisticsTask.java

@@ -0,0 +1,17 @@
+package com.fs.course.task;
+
+import com.fs.course.service.IFsUserCourseCompanyStatisticsService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+@Component("courseStatisticsTask")
+public class CourseStatisticsTask {
+    @Autowired
+    private IFsUserCourseCompanyStatisticsService fsUserCourseCompanyStatisticsService;
+
+    public void saveCourseStatisticsTask(Integer status,Integer day) {
+        fsUserCourseCompanyStatisticsService.courseDailyStatisticsTask(status,day);
+
+    }
+
+}

+ 37 - 0
fs-admin/src/main/java/com/fs/course/task/RedPacketLogsTask.java

@@ -0,0 +1,37 @@
+package com.fs.course.task;
+
+import com.fs.course.mapper.FsCourseRedPacketLogMapper;
+import com.fs.course.service.IFsCourseRedPacketLogService;
+import com.fs.course.service.impl.FsCourseRedPacketLogServiceImpl;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.time.LocalDateTime;
+
+/**
+ * @description: 微信红包转账回调部分没有接收到,主动自己去查询
+ * @author: Xgb
+ * @createDate: 2025/10/15
+ * @version: 1.0
+ */
+@Component("redPacketLogsTask")
+public class RedPacketLogsTask {
+
+    @Autowired
+    private IFsCourseRedPacketLogService fsCourseRedPacketLogService;
+
+    /**
+     * @Description: 查询微信红包转账结果 每10分钟查询上一个10分钟区间的红包转账结果
+     * (定时调取失败 可以启动fs-qw-task 中的 CommonController queryRedPacketResult 手动调取) 仅给内部人员使用
+     * @Param:
+     * @Return:
+     * @Author xgb
+     * @Date 2025/10/15 16:32
+     */
+    public void queryRedPacketResult() {
+
+        // 查询RedPacketLog表,
+        fsCourseRedPacketLogService.queryRedPacketResult(null, null);
+    }
+
+}

+ 31 - 0
fs-admin/src/main/java/com/fs/course/task/WatchCourseStatistics.java

@@ -0,0 +1,31 @@
+package com.fs.course.task;
+
+import com.fs.statis.service.IStatisticsService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+/**
+ * @description: 看客相关统计定时任务
+ * @author: Xgb
+ * @createDate: 2025/10/27
+ * @version: 1.0
+ */
+@Component("watchCourseStatistics")
+public class WatchCourseStatistics {
+
+    @Autowired
+    private IStatisticsService statisticsService;
+
+    /**
+     * @Description: 统计统计按TimeType 0-今天,1-昨天,2-本周,3-本月,4-上月;各公司的观看人数和完播人数,
+     * 各公司的观看人数和完播人数, 存到redis中,定时任务每15分钟执行一次
+     *
+     * @Param:
+     * @Return:
+     * @Author xgb
+     * @Date 2025/10/27 11:47
+     */
+    public void watchCourseStatisticsGroupByCompany() {
+        statisticsService.watchCourseStatisticsGroupByCompany();
+    }
+}

+ 15 - 13
fs-admin/src/main/java/com/fs/crm/controller/CrmCustomerController.java

@@ -151,14 +151,14 @@ public class CrmCustomerController extends BaseController
         crmCustomer.setIsLine(0);
         List<CrmCustomerListVO> list = crmCustomerService.selectCrmCustomerListVO(crmCustomer);
         List<CrmCustomerExportVO> exportList=new ArrayList<>();
-        boolean checkPhone = isCheckPhone();
+        SysRole sysRole = isCheckPermission();
         for(CrmCustomerListVO customer:list){
             CrmCustomerExportVO vo=new CrmCustomerExportVO();
             if(customer.getSource()!=null){
                 vo.setSource(customer.getSource().toString());
             }
             BeanUtils.copyProperties(customer,vo);
-            if(customer.getMobile()!=null && !checkPhone){
+            if(customer.getMobile()!=null && !(sysRole.getIsCheckPhone()==1)){
                 vo.setMobile(customer.getMobile().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"));
             }
             exportList.add(vo);
@@ -197,9 +197,9 @@ public class CrmCustomerController extends BaseController
         }
         List<CrmCustomerListVO> list = crmCustomerService.selectCrmCustomerListQueryParam(crmCustomer);
         if (list != null) {
-            boolean checkPhone = isCheckPhone();
+            SysRole sysRole = isCheckPermission();
             for (CrmCustomerListVO vo : list) {
-                if(vo.getMobile()!=null && !checkPhone){
+                if(vo.getMobile()!=null && !(sysRole.getIsCheckPhone()==1)){
                     vo.setMobile(vo.getMobile().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"));
                 }
 
@@ -211,19 +211,21 @@ public class CrmCustomerController extends BaseController
 
     @Autowired
     private ISysRoleService sysRoleService;
-    private boolean isCheckPhone() {
+    private SysRole isCheckPermission() {
+        SysRole sysRole = new SysRole();
         SysUser user = getLoginUser().getUser();
         boolean flag = user.isAdmin();
         if (flag) {
-            return true;
-        }
-        List<SysRole> roles = user.getRoles();
-        if (roles != null && !roles.isEmpty()) {
-            Long[] roleIds = roles.stream().map(SysRole::getRoleId).toArray(Long[]::new);
-            return sysRoleService.getIsCheckPhone(roleIds);
+            sysRole.setIsCheckPhone(1);
+            sysRole.setIsCheckAddress(1);
+        } else {
+            List<SysRole> roles = user.getRoles();
+            if (roles != null && !roles.isEmpty()) {
+                Long[] roleIds = roles.stream().map(SysRole::getRoleId).toArray(Long[]::new);
+                return sysRoleService.getIsCheckPermission(roleIds);
+            }
         }
-
-        return false;
+        return sysRole;
     }
 
 

+ 1 - 2
fs-admin/src/main/java/com/fs/fastGpt/FastgptExtUserTagController.java

@@ -4,7 +4,6 @@ import java.util.List;
 
 import com.fs.common.core.domain.R;
 import com.fs.fastGpt.vo.FastgptExtUserTagVO;
-import com.fs.framework.web.service.TokenService;
 import com.fs.qw.service.IQwCompanyService;
 import com.fs.qw.vo.QwOptionsVO;
 import org.springframework.security.access.prepost.PreAuthorize;
@@ -64,7 +63,7 @@ public class FastgptExtUserTagController extends BaseController
     @GetMapping("/getMyQwUserList")
     public R getMyQwUserList()
     {
-        List<QwOptionsVO> list = qwCompanyService.selectQwCompanyListOptionsVO();
+        List<QwOptionsVO> list = qwCompanyService.selectQwCompanyListOptionsVO(null, null);
         return  R.ok().put("data",list);
     }
 

+ 6 - 6
fs-admin/src/main/java/com/fs/his/controller/FsAdvController.java

@@ -40,7 +40,7 @@ public class FsAdvController extends BaseController
     /**
      * 查询广告列表
      */
-    @PreAuthorize("@ss.hasPermi('his:adv:list')")
+    @PreAuthorize("@ss.hasPermi('store:adv:list') or @ss.hasPermi('his:adv:list')")
     @GetMapping("/list")
     public TableDataInfo list(FsAdv fsAdv)
     {
@@ -52,7 +52,7 @@ public class FsAdvController extends BaseController
     /**
      * 导出广告列表
      */
-    @PreAuthorize("@ss.hasPermi('his:adv:export')")
+    @PreAuthorize("@ss.hasPermi('store:adv:export') or @ss.hasPermi('his:adv:export')")
     @Log(title = "广告", businessType = BusinessType.EXPORT)
     @GetMapping("/export")
     public AjaxResult export(FsAdv fsAdv)
@@ -65,7 +65,7 @@ public class FsAdvController extends BaseController
     /**
      * 获取广告详细信息
      */
-    @PreAuthorize("@ss.hasPermi('his:adv:query')")
+    @PreAuthorize("@ss.hasPermi('store:adv:query') or @ss.hasPermi('his:adv:query')")
     @GetMapping(value = "/{advId}")
     public AjaxResult getInfo(@PathVariable("advId") String advId)
     {
@@ -76,7 +76,7 @@ public class FsAdvController extends BaseController
     /**
      * 新增广告
      */
-    @PreAuthorize("@ss.hasPermi('his:adv:add')")
+    @PreAuthorize("@ss.hasPermi('store:adv:add') or @ss.hasPermi('his:adv:add')")
     @Log(title = "广告", businessType = BusinessType.INSERT)
     @PostMapping
     public AjaxResult add(@RequestBody FsAdv fsAdv)
@@ -91,7 +91,7 @@ public class FsAdvController extends BaseController
     /**
      * 修改广告
      */
-    @PreAuthorize("@ss.hasPermi('his:adv:edit')")
+    @PreAuthorize("@ss.hasPermi('store:adv:edit') or @ss.hasPermi('his:adv:edit')")
     @Log(title = "广告", businessType = BusinessType.UPDATE)
     @PutMapping
     public AjaxResult edit(@RequestBody FsAdv fsAdv)
@@ -105,7 +105,7 @@ public class FsAdvController extends BaseController
     /**
      * 删除广告
      */
-    @PreAuthorize("@ss.hasPermi('his:adv:remove')")
+    @PreAuthorize("@ss.hasPermi('store:adv:remove') or @ss.hasPermi('his:adv:remove')")
     @Log(title = "广告", businessType = BusinessType.DELETE)
 	@DeleteMapping("/{advIds}")
     public AjaxResult remove(@PathVariable String[] advIds)

+ 6 - 6
fs-admin/src/main/java/com/fs/his/controller/FsArticleController.java

@@ -42,7 +42,7 @@ public class FsArticleController extends BaseController
     /**
      * 查询文章列表
      */
-    @PreAuthorize("@ss.hasPermi('his:article:list')")
+    @PreAuthorize("@ss.hasPermi('his:healthArticle:list')")
     @GetMapping("/list")
     public TableDataInfo list(FsArticle fsArticle)
     {
@@ -54,7 +54,7 @@ public class FsArticleController extends BaseController
     /**
      * 导出文章列表
      */
-    @PreAuthorize("@ss.hasPermi('his:article:export')")
+    @PreAuthorize("@ss.hasPermi('his:healthArticle:export')")
     @Log(title = "文章", businessType = BusinessType.EXPORT)
     @GetMapping("/export")
     public AjaxResult export(FsArticle fsArticle)
@@ -67,7 +67,7 @@ public class FsArticleController extends BaseController
     /**
      * 获取文章详细信息
      */
-    @PreAuthorize("@ss.hasPermi('his:article:query')")
+    @PreAuthorize("@ss.hasPermi('his:healthArticle:query')")
     @GetMapping(value = "/{articleId}")
     public AjaxResult getInfo(@PathVariable("articleId") Long articleId)
     {
@@ -77,7 +77,7 @@ public class FsArticleController extends BaseController
     /**
      * 新增文章
      */
-    @PreAuthorize("@ss.hasPermi('his:article:add')")
+    @PreAuthorize("@ss.hasPermi('his:healthArticle:add')")
     @Log(title = "文章", businessType = BusinessType.INSERT)
     @PostMapping
     public AjaxResult add(@RequestBody FsArticle fsArticle)
@@ -94,7 +94,7 @@ public class FsArticleController extends BaseController
     /**
      * 修改文章
      */
-    @PreAuthorize("@ss.hasPermi('his:article:edit')")
+    @PreAuthorize("@ss.hasPermi('his:healthArticle:edit')")
     @Log(title = "文章", businessType = BusinessType.UPDATE)
     @PutMapping
     public AjaxResult edit(@RequestBody FsArticle fsArticle)
@@ -110,7 +110,7 @@ public class FsArticleController extends BaseController
     /**
      * 删除文章
      */
-    @PreAuthorize("@ss.hasPermi('his:article:remove')")
+    @PreAuthorize("@ss.hasPermi('his:healthArticle:remove')")
     @Log(title = "文章", businessType = BusinessType.DELETE)
 	@DeleteMapping("/{articleIds}")
     public AjaxResult remove(@PathVariable Long[] articleIds)

+ 144 - 9
fs-admin/src/main/java/com/fs/his/controller/FsCompanyController.java

@@ -2,7 +2,7 @@ package com.fs.his.controller;
 
 import java.util.List;
 
-import cn.hutool.core.util.IdUtil;
+import cn.hutool.json.JSONUtil;
 import com.alibaba.fastjson.JSON;
 import com.fs.common.annotation.RepeatSubmit;
 import com.fs.common.core.domain.R;
@@ -15,20 +15,20 @@ import com.fs.company.domain.Company;
 import com.fs.company.domain.CompanyDeduct;
 import com.fs.company.domain.CompanyRecharge;
 import com.fs.company.param.CompanyDeductParam;
+import com.fs.company.param.CompanyDivConfigUpdateParam;
 import com.fs.company.param.CompanyRechargeParam;
-import com.fs.company.service.ICompanyDeductService;
-import com.fs.company.service.ICompanyRechargeService;
-import com.fs.company.service.ICompanyService;
-import com.fs.company.service.ICompanyUserService;
+import com.fs.company.service.*;
 import com.fs.company.vo.CompanyVO;
 import com.fs.core.utils.OrderCodeUtils;
+import com.fs.course.config.CourseConfig;
 import com.fs.framework.web.service.TokenService;
-import com.fs.his.domain.FsDoctor;
 import com.fs.his.mapper.FsDoctorMapper;
 import com.fs.his.vo.OptionsVO;
+import com.fs.system.service.ISysConfigService;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.CollectionUtils;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.PutMapping;
@@ -51,8 +51,7 @@ import com.fs.common.core.page.TableDataInfo;
  */
 @RestController
 @RequestMapping("/his/company")
-public class FsCompanyController extends BaseController
-{
+public class FsCompanyController extends BaseController {
     @Autowired
     private ICompanyService companyService;
     @Autowired
@@ -67,6 +66,10 @@ public class FsCompanyController extends BaseController
     private FsDoctorMapper fsDoctorMapper;
     @Autowired
     private ICompanyDeductService deductService;
+    @Autowired
+    private ISysConfigService configService;
+    @Autowired
+    private ICompanyDivConfigService companyDivConfigService;
     /**
      * 查询诊所管理列表
      */
@@ -75,6 +78,12 @@ public class FsCompanyController extends BaseController
     public TableDataInfo list(Company company)
     {
         startPage();
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        String json = configService.selectConfigByKey("course.config");
+        CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
+        if(!loginUser.isAdmin() && config.getDept() != null && config.getDept()){
+            company.setDeptId(loginUser.getDeptId());
+        }
         List<CompanyVO> list = companyService.selectCompanyListVO(company);
         return getDataTable(list);
     }
@@ -83,7 +92,14 @@ public class FsCompanyController extends BaseController
     public R companyList()
     {
 
-        List<OptionsVO> list = companyService.selectAllCompanyList();
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long depId = null;
+        String json = configService.selectConfigByKey("course.config");
+        CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
+        if(!loginUser.isAdmin() && config.getDept() != null && config.getDept()){
+            depId = loginUser.getDeptId();
+        }
+        List<OptionsVO> list = companyService.selectAllCompanyList(depId);
         return R.ok().put("data",list);
     }
     /**
@@ -133,6 +149,12 @@ public class FsCompanyController extends BaseController
     @PostMapping
     public R add(@RequestBody Company company)
     {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        String json = configService.selectConfigByKey("course.config");
+        CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
+        if(!loginUser.isAdmin() && config.getDept() != null && config.getDept()){
+            company.setDeptId(loginUser.getDeptId());
+        }
         return companyService.insertCompany(company);
     }
 
@@ -195,6 +217,65 @@ public class FsCompanyController extends BaseController
 
     }
 
+
+    /**
+     * @Description: 红包充值功能 因公司余额的变动关联的地方太多,现在把充值红包的金额单独提出来 不合计到公司余额中
+     * @Param:
+     * @Return:
+     * @Author xgb
+     * @Date 2025/11/3 11:08
+     */
+    @PreAuthorize("@ss.hasPermi('his:company:redRecharge')")
+    @Log(title = "红包充值", businessType = BusinessType.INSERT)
+    @PostMapping(value = "/redRecharge")
+    @Transactional
+    @RepeatSubmit
+    public R redRecharge(@RequestBody CompanyRechargeParam param)
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        CompanyRecharge redRecharge=new CompanyRecharge();
+        String orderSn =  OrderCodeUtils.getOrderSn();
+        if(StringUtils.isEmpty(orderSn)){
+            return R.error("订单生成失败,请重试");
+        }
+        redRecharge.setRechargeNo(orderSn);
+        redRecharge.setCompanyId(param.getCompanyId());
+        redRecharge.setMoney(param.getMoney());
+        redRecharge.setCreateUserId(loginUser.getUser().getUserId());
+        redRecharge.setIsAudit(0);
+        redRecharge.setStatus(0);
+        redRecharge.setRemark(param.getRemark());
+        redRecharge.setPayType(3);
+        redRecharge.setBusinessType(1);// 红包充值
+        rechargeService.insertCompanyRecharge(redRecharge);
+        return R.ok("提交成功,等待审核");
+
+    }
+
+    @PreAuthorize("@ss.hasPermi('his:company:redDeduct')")
+    @Log(title = "企业扣款", businessType = BusinessType.INSERT)
+    @PostMapping(value = "/redDeduct")
+    @Transactional
+    @RepeatSubmit
+    public R redDeduct(@RequestBody CompanyDeductParam param)
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        CompanyDeduct deduct=new CompanyDeduct();
+        String orderSn =  OrderCodeUtils.getOrderSn();
+        if(StringUtils.isEmpty(orderSn)){
+            return R.error("订单生成失败,请重试");
+        }
+        deduct.setDeductNo(orderSn);
+        deduct.setCompanyId(param.getCompanyId());
+        deduct.setMoney(param.getMoney());
+        deduct.setCreateUserId(loginUser.getUser().getUserId());
+        deduct.setIsAudit(0);
+        deduct.setRemark(param.getRemark());
+        deduct.setBusinessType(1); // 红包扣款
+        deductService.insertCompanyDeduct(deduct);
+        return R.ok("提交成功,等待审核");
+    }
+
     @PreAuthorize("@ss.hasPermi('his:company:deduct')")
     @Log(title = "企业扣款", businessType = BusinessType.INSERT)
     @PostMapping(value = "/deduct")
@@ -219,4 +300,58 @@ public class FsCompanyController extends BaseController
         deductService.insertCompanyDeduct(deduct);
         return R.ok("提交成功,等待审核");
     }
+
+    /**
+     * 获取公司分账配置详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('his:companyDivConfig:query')")
+    @GetMapping(value = "/getDivConfig/{companyId}")
+    public AjaxResult getDivConfig(@PathVariable("companyId") Long companyId)
+    {
+        return AjaxResult.success(companyDivConfigService.selectCompanyDivConfigByCompanyId(companyId));
+    }
+
+    /**
+     * 公司分账配置
+     */
+    @PreAuthorize("@ss.hasPermi('his:companyDivConfig:set')")
+    @Log(title = "公司分账配置", businessType = BusinessType.UPDATE)
+    @PostMapping("/setDiv")
+    public R setDiv(@RequestBody CompanyDivConfigUpdateParam param)
+    {
+        return companyDivConfigService.setDiv(param);
+    }
+
+    @PostMapping("/exitMiniProgram")
+    @Log(title = "批量修改小程序", businessType = BusinessType.UPDATE)
+    public R exitMiniProgram(@RequestBody Company company) {
+        // 1. 前置校验:核心参数非空校验
+        if (CollectionUtils.isEmpty(company.getIds())) {
+            return R.error("请选择需要修改的企业ID");
+        }
+        if (isNNull(company.getCourseMiniAppId())&&isListNull(company.getMiniAppMaster()) && isListNull(company.getMiniAppServer())) {
+            return R.error("请选择要修改的小程序");
+        } else {
+            for (Long id : company.getIds()) {
+                Company c = companyService.selectCompanyById(id);
+                c.setMoney(null);
+                c.setUpdateMiniApp(true);
+                if (company.getCourseMiniAppId()!= null&&!company.getCourseMiniAppId().isEmpty()) c.setCourseMiniAppId(company.getCourseMiniAppId());
+                if (company.getMiniAppMaster()!=null&&!company.getMiniAppMaster().isEmpty()) c.setMiniAppMaster(company.getMiniAppMaster());
+                if (company.getMiniAppServer()!=null&&!company.getMiniAppServer().isEmpty()) c.setMiniAppServer(company.getMiniAppServer());
+                companyService.updateCompany(c);
+            }
+            return R.ok();
+        }
+    }
+   public boolean isNNull(String value){
+        if (value==null)return true;
+        if (value.isEmpty())return true;
+        return false;
+    }
+    public boolean isListNull(List<String> value){
+        if (value==null)return true;
+        if (value.isEmpty())return true;
+        return false;
+    }
 }

+ 18 - 13
fs-admin/src/main/java/com/fs/his/controller/FsCompanyDeductController.java

@@ -139,19 +139,24 @@ public class FsCompanyDeductController extends BaseController
         deduct.setRemark(param.getRemark());
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         if(deduct.getIsAudit()==1){
-            Company company=companyService.selectCompanyByIdForUpdate(deduct.getCompanyId());
-            company.setMoney(company.getMoney().subtract(deduct.getMoney()));
-            deduct.setBalance(company.getMoney());
-            companyService.updateCompany(company);
-            //生成流水
-            CompanyMoneyLogs log=new CompanyMoneyLogs();
-            log.setCompanyId(deduct.getCompanyId());
-            log.setMoney(deduct.getMoney().multiply(new BigDecimal(-1)));
-            log.setRemark(deduct.getRemark());
-            log.setLogsType(2);
-            log.setBalance(company.getMoney());
-            log.setCreateTime(new Date());
-            moneyLogsService.insertCompanyMoneyLogs(log);
+            if(1==param.getBusinessType()){// 红包充值
+                // 更新红包充值余额redis字段
+                companyService.redPacketTopUpCompany(deduct.getCompanyId(),deduct.getMoney(),"2");
+            }else {
+                Company company=companyService.selectCompanyByIdForUpdate(deduct.getCompanyId());
+                company.setMoney(company.getMoney().subtract(deduct.getMoney()));
+                deduct.setBalance(company.getMoney());
+                companyService.updateCompany(company);
+                //生成流水
+                CompanyMoneyLogs log=new CompanyMoneyLogs();
+                log.setCompanyId(deduct.getCompanyId());
+                log.setMoney(deduct.getMoney().multiply(new BigDecimal(-1)));
+                log.setRemark(deduct.getRemark());
+                log.setLogsType(2);
+                log.setBalance(company.getMoney());
+                log.setCreateTime(new Date());
+                moneyLogsService.insertCompanyMoneyLogs(log);
+            }
         }
         deduct.setAuditTime(new Date());
         deduct.setAuditUserId(loginUser.getUser().getUserId());

+ 127 - 0
fs-admin/src/main/java/com/fs/his/controller/FsCompanyDivItemController.java

@@ -0,0 +1,127 @@
+package com.fs.his.controller;
+
+import java.util.List;
+
+import com.fs.common.core.domain.R;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.enums.BusinessType;
+import com.fs.company.domain.CompanyDivItem;
+import com.fs.company.service.ICompanyDivItemService;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.common.core.page.TableDataInfo;
+
+/**
+ * 分账明细Controller
+ *
+ * @author fs
+ * @date 2025-10-21
+ */
+@RestController
+@RequestMapping("/his/divItem")
+public class FsCompanyDivItemController extends BaseController
+{
+    @Autowired
+    private ICompanyDivItemService companyDivItemService;
+
+    /**
+     * 查询分账明细列表
+     */
+    @PreAuthorize("@ss.hasPermi('his:divItem:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(CompanyDivItem companyDivItem)
+    {
+        startPage();
+        List<CompanyDivItem> list = companyDivItemService.selectCompanyDivItemList(companyDivItem);
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出分账明细列表
+     */
+    @PreAuthorize("@ss.hasPermi('his:divItem:export')")
+    @Log(title = "分账明细", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(CompanyDivItem companyDivItem)
+    {
+        List<CompanyDivItem> list = companyDivItemService.selectCompanyDivItemList(companyDivItem);
+        ExcelUtil<CompanyDivItem> util = new ExcelUtil<CompanyDivItem>(CompanyDivItem.class);
+        return util.exportExcel(list, "分账明细数据");
+    }
+
+    /**
+     * 获取分账明细详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('his:divItem:query')")
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable("id") Long id)
+    {
+        return AjaxResult.success(companyDivItemService.selectCompanyDivItemById(id));
+    }
+
+    /**
+     * 分账确认
+     */
+    @PreAuthorize("@ss.hasPermi('his:divItem:confirm')")
+    @Log(title = "延迟分账确认", businessType = BusinessType.UPDATE)
+    @PostMapping("/confirm")
+    public R confirm(@RequestBody List<String> payCodes)
+    {
+        return companyDivItemService.confirmByPayCodes(payCodes);
+    }
+
+    /**
+     * 分账确认Scrm
+     */
+    @PreAuthorize("@ss.hasPermi('his:divItem:confirm')")
+    @Log(title = "延迟分账确认", businessType = BusinessType.UPDATE)
+    @PostMapping("/confirmScrm")
+    public R confirmScrm(@RequestBody List<String> payCodes)
+    {
+        return companyDivItemService.confirmByPayCodesScrm(payCodes);
+    }
+
+//    /**
+//     * 新增分账明细
+//     */
+//    @PreAuthorize("@ss.hasPermi('his:divItem:add')")
+//    @Log(title = "分账明细", businessType = BusinessType.INSERT)
+//    @PostMapping
+//    public AjaxResult add(@RequestBody CompanyDivItem companyDivItem)
+//    {
+//        return toAjax(companyDivItemService.insertCompanyDivItem(companyDivItem));
+//    }
+//
+//    /**
+//     * 修改分账明细
+//     */
+//    @PreAuthorize("@ss.hasPermi('his:divItem:edit')")
+//    @Log(title = "分账明细", businessType = BusinessType.UPDATE)
+//    @PutMapping
+//    public AjaxResult edit(@RequestBody CompanyDivItem companyDivItem)
+//    {
+//        return toAjax(companyDivItemService.updateCompanyDivItem(companyDivItem));
+//    }
+//
+//    /**
+//     * 删除分账明细
+//     */
+//    @PreAuthorize("@ss.hasPermi('his:divItem:remove')")
+//    @Log(title = "分账明细", businessType = BusinessType.DELETE)
+//	@DeleteMapping("/{ids}")
+//    public AjaxResult remove(@PathVariable Long[] ids)
+//    {
+//        return toAjax(companyDivItemService.deleteCompanyDivItemByIds(ids));
+//    }
+}

+ 20 - 13
fs-admin/src/main/java/com/fs/his/controller/FsCompanyRechargeController.java

@@ -137,21 +137,28 @@ public class FsCompanyRechargeController extends BaseController
         companyRecharge.setRemark(param.getRemark());
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         if(companyRecharge.getIsAudit()==1){
-            Company company=companyService.selectCompanyByIdForUpdate(companyRecharge.getCompanyId());
-            company.setMoney(company.getMoney().add(companyRecharge.getMoney()));
-            companyRecharge.setBalance(company.getMoney());
-            companyService.updateCompany(company);
-            //生成流水
-            CompanyMoneyLogs log=new CompanyMoneyLogs();
-            log.setCompanyId(companyRecharge.getCompanyId());
-            log.setMoney(companyRecharge.getMoney());
-            log.setRemark(companyRecharge.getRemark());
-            log.setLogsType(1);
-            log.setBalance(company.getMoney());
-            log.setCreateTime(new Date());
-            moneyLogsService.insertCompanyMoneyLogs(log);
+            if(1==companyRecharge.getBusinessType()){// 红包充值
+                // 更新红包充值余额redis字段
+                companyService.redPacketTopUpCompany(companyRecharge.getCompanyId(),companyRecharge.getMoney(),"1");
+            }else {
+                Company company=companyService.selectCompanyByIdForUpdate(companyRecharge.getCompanyId());
+                company.setMoney(company.getMoney().add(companyRecharge.getMoney()));
+                companyRecharge.setBalance(company.getMoney());
+                companyService.updateCompany(company);
+                //生成流水
+                CompanyMoneyLogs log=new CompanyMoneyLogs();
+                log.setCompanyId(companyRecharge.getCompanyId());
+                log.setMoney(companyRecharge.getMoney());
+                log.setRemark(companyRecharge.getRemark());
+                log.setLogsType(1);
+                log.setBalance(company.getMoney());
+                log.setCreateTime(new Date());
+                moneyLogsService.insertCompanyMoneyLogs(log);
+
+            }
             companyRecharge.setPayTime(new Date());
             companyRecharge.setStatus(1);
+
         }
 
         companyRecharge.setAuditTime(new Date());

+ 124 - 0
fs-admin/src/main/java/com/fs/his/controller/FsComplaintTemplateController.java

@@ -0,0 +1,124 @@
+package com.fs.his.controller;
+
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.domain.R;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.his.domain.FsComplaintTemplate;
+import com.fs.his.service.IFsComplaintTemplateService;
+import com.fs.his.utils.ComplaintTreeUtil;
+import com.fs.his.vo.FsComplaintTemplateVO;
+import com.google.common.collect.Lists;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 投诉模板Controller
+ *
+ * @author fs
+ * @date 2025-06-09
+ */
+@RestController
+@RequestMapping("/his/template")
+public class FsComplaintTemplateController extends BaseController
+{
+    @Autowired
+    private IFsComplaintTemplateService fsComplaintTemplateService;
+
+    /**
+     * 查询投诉模板列表
+     */
+    @PreAuthorize("@ss.hasPermi('his:template:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(FsComplaintTemplate fsComplaintTemplate)
+    {
+        startPage();
+        fsComplaintTemplate.setIsDel(0L);
+        List<FsComplaintTemplate> list = fsComplaintTemplateService.selectFsComplaintTemplateList(fsComplaintTemplate);
+        return getDataTable(list);
+    }
+
+    /**
+     * 查询投诉模板树形结果
+     */
+    @PreAuthorize("@ss.hasPermi('his:template:list')")
+    @GetMapping("/treeList")
+    public R treeList(FsComplaintTemplate fsComplaintTemplate)
+    {
+        List<FsComplaintTemplate> list = fsComplaintTemplateService.selectFsComplaintTemplateList(fsComplaintTemplate);
+        List<FsComplaintTemplateVO> templateVOS = Lists.newArrayList();
+        for (FsComplaintTemplate template : list){
+            FsComplaintTemplateVO templateVO = new FsComplaintTemplateVO();
+            templateVO.setId(template.getId());
+            templateVO.setName(template.getName());
+            templateVO.setParentId(template.getParentId());
+            templateVO.setDescription(template.getDescription());
+            templateVO.setSort(template.getSort());
+            templateVOS.add(templateVO);
+        }
+        return R.ok().put("data", ComplaintTreeUtil.list2TreeConverter(templateVOS, 0));
+    }
+
+
+    /**
+     * 导出投诉模板列表
+     */
+    @PreAuthorize("@ss.hasPermi('his:template:export')")
+    @Log(title = "投诉模板", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(FsComplaintTemplate fsComplaintTemplate)
+    {
+        List<FsComplaintTemplate> list = fsComplaintTemplateService.selectFsComplaintTemplateList(fsComplaintTemplate);
+        ExcelUtil<FsComplaintTemplate> util = new ExcelUtil<FsComplaintTemplate>(FsComplaintTemplate.class);
+        return util.exportExcel(list, "投诉模板数据");
+    }
+
+    /**
+     * 获取投诉模板详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('his:template:query')")
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable("id") Long id)
+    {
+        return AjaxResult.success(fsComplaintTemplateService.selectFsComplaintTemplateById(id));
+    }
+
+    /**
+     * 新增投诉模板
+     */
+    @PreAuthorize("@ss.hasPermi('his:template:add')")
+    @Log(title = "投诉模板", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody FsComplaintTemplate fsComplaintTemplate)
+    {
+        return toAjax(fsComplaintTemplateService.insertFsComplaintTemplate(fsComplaintTemplate));
+    }
+
+    /**
+     * 修改投诉模板
+     */
+    @PreAuthorize("@ss.hasPermi('his:template:edit')")
+    @Log(title = "投诉模板", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody FsComplaintTemplate fsComplaintTemplate)
+    {
+        return toAjax(fsComplaintTemplateService.updateFsComplaintTemplate(fsComplaintTemplate));
+    }
+
+    /**
+     * 删除投诉模板
+     */
+    @PreAuthorize("@ss.hasPermi('his:template:remove')")
+    @Log(title = "投诉模板", businessType = BusinessType.DELETE)
+	@DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids)
+    {
+        return toAjax(fsComplaintTemplateService.deleteFsComplaintTemplateByIds(ids));
+    }
+}

+ 103 - 0
fs-admin/src/main/java/com/fs/his/controller/FsDfAccountController.java

@@ -0,0 +1,103 @@
+package com.fs.his.controller;
+
+import java.util.List;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.enums.BusinessType;
+import com.fs.his.domain.FsDfAccount;
+import com.fs.his.service.IFsDfAccountService;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.common.core.page.TableDataInfo;
+
+/**
+ * 代服账户Controller
+ *
+ * @author fs
+ * @date 2025-10-13
+ */
+@RestController
+@RequestMapping("/his/dfAccount")
+public class FsDfAccountController extends BaseController
+{
+    @Autowired
+    private IFsDfAccountService fsDfAccountService;
+
+    /**
+     * 查询代服账户列表
+     */
+
+    @GetMapping("/list")
+    public TableDataInfo list(FsDfAccount fsDfAccount)
+    {
+        startPage();
+        List<FsDfAccount> list = fsDfAccountService.selectFsDfAccountList(fsDfAccount);
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出代服账户列表
+     */
+    @PreAuthorize("@ss.hasPermi('his:dfAccount:export')")
+    @Log(title = "代服账户", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(FsDfAccount fsDfAccount)
+    {
+        List<FsDfAccount> list = fsDfAccountService.selectFsDfAccountList(fsDfAccount);
+        ExcelUtil<FsDfAccount> util = new ExcelUtil<FsDfAccount>(FsDfAccount.class);
+        return util.exportExcel(list, "代服账户数据");
+    }
+
+    /**
+     * 获取代服账户详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('his:dfAccount:query')")
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable("id") Long id)
+    {
+        return AjaxResult.success(fsDfAccountService.selectFsDfAccountById(id));
+    }
+
+    /**
+     * 新增代服账户
+     */
+    @PreAuthorize("@ss.hasPermi('his:dfAccount:add')")
+    @Log(title = "代服账户", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody FsDfAccount fsDfAccount)
+    {
+        return toAjax(fsDfAccountService.insertFsDfAccount(fsDfAccount));
+    }
+
+    /**
+     * 修改代服账户
+     */
+    @PreAuthorize("@ss.hasPermi('his:dfAccount:edit')")
+    @Log(title = "代服账户", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody FsDfAccount fsDfAccount)
+    {
+        return toAjax(fsDfAccountService.updateFsDfAccount(fsDfAccount));
+    }
+
+    /**
+     * 删除代服账户
+     */
+    @PreAuthorize("@ss.hasPermi('his:dfAccount:remove')")
+    @Log(title = "代服账户", businessType = BusinessType.DELETE)
+	@DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids)
+    {
+        return toAjax(fsDfAccountService.deleteFsDfAccountByIds(ids));
+    }
+}

+ 49 - 4
fs-admin/src/main/java/com/fs/his/controller/FsDoctorController.java

@@ -1,17 +1,19 @@
 package com.fs.his.controller;
 
 import java.util.Base64;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
+import com.fs.common.core.domain.R;
 import com.fs.common.utils.DateUtils;
 import com.fs.common.utils.SecurityUtils;
 import com.fs.common.utils.sign.Md5Utils;
 import com.fs.his.param.*;
 import com.fs.his.utils.RedisCacheUtil;
-import com.fs.his.vo.FsDoctorListVO;
-import com.fs.his.vo.FsDoctorVO;
-import com.fs.his.vo.OptionsVO;
-import com.fs.his.vo.UserVo;
+import com.fs.his.vo.*;
+import com.github.pagehelper.PageHelper;
+import com.github.pagehelper.PageInfo;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
@@ -24,6 +26,9 @@ import com.fs.his.service.IFsDoctorService;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.common.core.page.TableDataInfo;
 
+import static com.fs.his.utils.PhoneUtil.decryptAutoPhoneMk;
+import static com.fs.his.utils.PhoneUtil.encryptPhone;
+
 /**
  * 医生管理Controller
  *
@@ -269,4 +274,44 @@ public class FsDoctorController extends BaseController
         return toAjax(fsDoctorService.updateFsDoctor(doc));
     }
 
+    @GetMapping("/getChooseDoctorList")
+    public R getChooseDoctorList(@RequestParam(required = false) String doctorName,
+                                 @RequestParam(required = false) Long hospitalId,
+                                 @RequestParam(required = false) Long deptId,
+                                 @RequestParam(required = false) String position,
+                                 @RequestParam(required = false) String mobile,
+                                 @RequestParam(required = false, defaultValue = "1") Integer pageNum,
+                                 @RequestParam(required = false, defaultValue = "10") Integer pageSize) {
+        Map<String,Object> params = new HashMap<>();
+        params.put("doctorName", doctorName);
+        params.put("hospitalId", hospitalId);
+        params.put("deptId", deptId);
+        params.put("position", position);
+        params.put("mobile", mobile);
+
+        PageHelper.startPage(pageNum, pageSize);
+        List<FsDoctorChooseVO> list = fsDoctorService.getChooseDoctorListByMap(params);
+        return R.ok().put("data", new PageInfo<>(list));
+    }
+
+
+    @GetMapping("/getDocVoList")
+    public TableDataInfo getDocVoList(FsDoctorParam param) {
+        startPage();
+        List<FsDoctorVO> list = fsDoctorService.selectDocVOByNameAndPhone(param);
+        if (list == null || list.isEmpty()) {
+            param.setMobile(encryptPhone(param.getMobile()));
+            list = fsDoctorService.selectDocVOByNameAndPhone(param);
+        }
+
+        if (list != null) {
+            list.forEach( item -> {
+                if (item.getMobile() != null)
+                    item.setMobile(decryptAutoPhoneMk(item.getMobile()));
+            });
+        }
+        return getDataTable(list);
+    }
+
+
 }

+ 98 - 0
fs-admin/src/main/java/com/fs/his/controller/FsHisComplaintController.java

@@ -0,0 +1,98 @@
+package com.fs.his.controller;
+
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.his.domain.FsHisComplaint;
+import com.fs.his.service.IFsHisComplaintService;
+import com.fs.his.vo.FsUserComplaintVo;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 用户投诉Controller
+ *
+ * @author fs
+ * @date 2025-06-09
+ */
+@RestController
+@RequestMapping("/his/complaint")
+public class FsHisComplaintController extends BaseController
+{
+    @Autowired
+    private IFsHisComplaintService fsUserComplaintService;
+
+    /**
+     * 查询用户投诉列表
+     */
+    @PreAuthorize("@ss.hasPermi('his:complaint:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(FsHisComplaint fsHisComplaint)
+    {
+        startPage();
+        List<FsUserComplaintVo> list = fsUserComplaintService.selectFsUserComplaintList(fsHisComplaint);
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出用户投诉列表
+     */
+    @PreAuthorize("@ss.hasPermi('his:complaint:export')")
+    @Log(title = "用户投诉", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(FsHisComplaint fsHisComplaint)
+    {
+        List<FsUserComplaintVo> list = fsUserComplaintService.selectFsUserComplaintList(fsHisComplaint);
+        ExcelUtil<FsUserComplaintVo> util = new ExcelUtil<FsUserComplaintVo>(FsUserComplaintVo.class);
+        return util.exportExcel(list, "用户投诉数据");
+    }
+
+    /**
+     * 获取用户投诉详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('his:complaint:query')")
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable("id") Long id)
+    {
+        return AjaxResult.success(fsUserComplaintService.selectFsUserComplaintById(id));
+    }
+
+    /**
+     * 新增用户投诉
+     */
+    @PreAuthorize("@ss.hasPermi('his:complaint:add')")
+    @Log(title = "用户投诉", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody FsHisComplaint fsHisComplaint)
+    {
+        return toAjax(fsUserComplaintService.insertFsUserComplaint(fsHisComplaint));
+    }
+
+    /**
+     * 修改用户投诉
+     */
+    @PreAuthorize("@ss.hasPermi('his:complaint:edit')")
+    @Log(title = "用户投诉", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody FsHisComplaint fsHisComplaint)
+    {
+        return toAjax(fsUserComplaintService.updateFsUserComplaint(fsHisComplaint));
+    }
+
+    /**
+     * 删除用户投诉
+     */
+    @PreAuthorize("@ss.hasPermi('his:complaint:remove')")
+    @Log(title = "用户投诉", businessType = BusinessType.DELETE)
+	@DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids)
+    {
+        return toAjax(fsUserComplaintService.deleteFsUserComplaintByIds(ids));
+    }
+}

Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini