136 Коммиты a262857a8e ... 26f42cec03

Автор SHA1 Сообщение Дата
  liangwenxuan 26f42cec03 修改 3 недель назад
  liangwenxuan 062d7d30d4 新增 3 недель назад
  liangwenxuan af33b2d314 新增 4 недель назад
  liangwenxuan 928db4fe37 修改 1 месяц назад
  liangwenxuan 106b550e67 新增 1 месяц назад
  liangwenxuan 723a314861 修改 1 месяц назад
  liangwenxuan d899550294 新增 1 месяц назад
  liangwenxuan 2a9b443322 修改 1 месяц назад
  liangwenxuan bb03f471c9 修改 1 месяц назад
  liangwenxuan d731a90696 新增 1 месяц назад
  liangwenxuan c666b63e86 修改 2 месяцев назад
  liangwenxuan ccd0b99bf8 修改 2 месяцев назад
  liangwenxuan dc1ef6c654 新增 2 месяцев назад
  liangwenxuan 6868c07927 新增 2 месяцев назад
  liangwenxuan cbf3b94adf 修改 2 месяцев назад
  liangwenxuan ed53418a06 新增 2 месяцев назад
  liangwenxuan fbe713d6d6 新增 2 месяцев назад
  liangwenxuan 3b6312e1fd 新增 2 месяцев назад
  liangwenxuan d23f999475 调整 2 месяцев назад
  liangwenxuan 3a3c3b4357 新增 2 месяцев назад
  liangwenxuan f4ba1fc9eb 调整 2 месяцев назад
  liangwenxuan 17647a37c3 新增 2 месяцев назад
  liangwenxuan aa0147afc6 调整 2 месяцев назад
  liangwenxuan cd9c729755 修改 2 месяцев назад
  liangwenxuan 590559a518 增加 2 месяцев назад
  liangwenxuan ebb20ac865 增加 2 месяцев назад
  liangwenxuan 97a755312e 增加 2 месяцев назад
  liangwenxuan e704d92cd9 增加 2 месяцев назад
  liangwenxuan 4bcae0f3a8 修改 2 месяцев назад
  liangwenxuan bf1f1f3d92 修复 2 месяцев назад
  liangwenxuan a953e78072 新增 2 месяцев назад
  liangwenxuan 774023eac4 新增 2 месяцев назад
  liangwenxuan 5331e5a13d 修改 2 месяцев назад
  liangwenxuan 202ba333bc 修改 3 месяцев назад
  liangwenxuan 293f494cae BUGFIX 3 месяцев назад
  liangwenxuan 4446b8df22 新增 3 месяцев назад
  liangwenxuan c591791ae3 修改 3 месяцев назад
  liangwenxuan c07e25603f 修改 3 месяцев назад
  liangwenxuan 234f4e5ced 新增 3 месяцев назад
  liangwenxuan 7ee3b008ba 外出审批调整 3 месяцев назад
  liangwenxuan bb7d5821f3 新增 3 месяцев назад
  liangwenxuan 3c29184297 修改 3 месяцев назад
  liangwenxuan 4ab0322ba3 修改 3 месяцев назад
  liangwenxuan 2a19313e9b 新增 3 месяцев назад
  liangwenxuan 530414ca7e BUGFIX 3 месяцев назад
  liangwenxuan ad821857c9 修改 3 месяцев назад
  liangwenxuan b9758e155d 新增 3 месяцев назад
  liangwenxuan a5a89b7306 补充提交 3 месяцев назад
  liangwenxuan 9c7f8933dd BUGFIX 3 месяцев назад
  liangwenxuan 5c07f3aa97 BUGFIX 3 месяцев назад
  liangwenxuan 9d2a18b5e1 新增 4 месяцев назад
  liangwenxuan ec9d9e6292 新增 4 месяцев назад
  liangwenxuan cc6eb9543c 新增 4 месяцев назад
  liangwenxuan 97b4f78a7b 修改 4 месяцев назад
  liangwenxuan efb94d951c 新增 4 месяцев назад
  liangwenxuan b145c328d6 修改 4 месяцев назад
  liangwenxuan ca6de50e4e 修改 4 месяцев назад
  liangwenxuan effa390ddf 新增 4 месяцев назад
  liangwenxuan dbd3d5a8ae 修改 4 месяцев назад
  liangwenxuan 739d10c9ab 新增 4 месяцев назад
  liangwenxuan 25d170afb7 新增 4 месяцев назад
  liangwenxuan d11addee3a 修改 4 месяцев назад
  liangwenxuan 48e5fd08c4 BUGFIX 4 месяцев назад
  liangwenxuan 2e079753b4 修改 4 месяцев назад
  liangwenxuan 58d4428cbd 修改 4 месяцев назад
  liangwenxuan 5fa992fd5c 新增 4 месяцев назад
  liangwenxuan 35359bf4aa 新增 4 месяцев назад
  liangwenxuan 5d675edb87 测试提交 4 месяцев назад
  liangwenxuan ecf85d3d90 BUGFIX 4 месяцев назад
  liangwenxuan 97c59de258 修改 4 месяцев назад
  liangwenxuan d191b464d8 新增 4 месяцев назад
  liangwenxuan b18c0e8354 BUGFIX 4 месяцев назад
  liangwenxuan e13917bc08 BUGFIX 4 месяцев назад
  liangwenxuan 8cc86d05fc 修改 4 месяцев назад
  liangwenxuan 086c1777ed 新增 4 месяцев назад
  liangwenxuan 683407aff8 BUGFIX 4 месяцев назад
  liangwenxuan 31c3f9700d 修改 4 месяцев назад
  liangwenxuan 71311b0326 BUGFIX 5 месяцев назад
  liangwenxuan 64b28de4f6 补充提交 5 месяцев назад
  liangwenxuan 1f44e6aff7 新增 5 месяцев назад
  liangwenxuan e221cc5378 补充提交 5 месяцев назад
  liangwenxuan 8ac6d56258 新增 5 месяцев назад
  liangwenxuan 6c450f3c98 新增 5 месяцев назад
  liangwenxuan 7567a5256b BUGFIX 5 месяцев назад
  liangwenxuan 5a1d8c90ea 新增 5 месяцев назад
  liangwenxuan 929dc32f4e 新增 5 месяцев назад
  liangwenxuan 3d02d6b4a4 新增 5 месяцев назад
  liangwenxuan 3b67ce1ad3 补充提交 5 месяцев назад
  liangwenxuan 049f37b9f6 新增 5 месяцев назад
  liangwenxuan c0b575aca8 新增 5 месяцев назад
  liangwenxuan 06f96ec67d 新增 5 месяцев назад
  liangwenxuan 6858587079 修改 5 месяцев назад
  liangwenxuan 2c2d190db6 新增 5 месяцев назад
  liangwenxuan 21b2992ca7 增加 5 месяцев назад
  liangwenxuan 9ecc7b7e94 增加 5 месяцев назад
  liangwenxuan 0923d3b1f1 补充提交 5 месяцев назад
  liangwenxuan fa0061682d 修改 5 месяцев назад
  liangwenxuan 3b3e36cf79 新增 5 месяцев назад
  liangwenxuan b9ddb331e8 修改 5 месяцев назад
  liangwenxuan 8d6261deab 新增 5 месяцев назад
  liangwenxuan e99ce0a9a5 新增 5 месяцев назад
  liangwenxuan 003fcb245f 新增 5 месяцев назад
  liangwenxuan 122d523cfe 新增 5 месяцев назад
  liangwenxuan e529045d47 补充提交 5 месяцев назад
  liangwenxuan e07c162ae3 新增 5 месяцев назад
  liangwenxuan e67407f69e 新增 6 месяцев назад
  liangwenxuan aa1c290ea6 修改 6 месяцев назад
  liangwenxuan 640555b566 新增 6 месяцев назад
  liangwenxuan 78891f8d2e 新增 6 месяцев назад
  liangwenxuan ee6f99fa19 新增 6 месяцев назад
  liangwenxuan 5e1714b6e0 新增修改 6 месяцев назад
  liangwenxuan 17346cf7da 新增 6 месяцев назад
  liangwenxuan 43e41548a4 BUGFIX 6 месяцев назад
  liangwenxuan 06c743c597 修改 6 месяцев назад
  liangwenxuan aac4bc0024 新增 6 месяцев назад
  liangwenxuan 001a3b2c48 修改 6 месяцев назад
  liangwenxuan 828923da22 新增 6 месяцев назад
  liangwenxuan a437797195 新增 6 месяцев назад
  liangwenxuan b985078aac 新增 6 месяцев назад
  liangwenxuan 0b7b253577 BUGFIX 6 месяцев назад
  liangwenxuan 78c56263f4 修改 6 месяцев назад
  liangwenxuan d61c301343 BUGFIX 6 месяцев назад
  liangwenxuan ad373eacb7 修改 6 месяцев назад
  liangwenxuan 8c03982e56 新增 6 месяцев назад
  liangwenxuan d8b01f8282 增加 6 месяцев назад
  liangwenxuan 82f823c437 增加 6 месяцев назад
  liangwenxuan 4d2628fcff 修改 6 месяцев назад
  liangwenxuan c941784012 补充提交 6 месяцев назад
  liangwenxuan ca78e154fa 新增 7 месяцев назад
  liangwenxuan 48d38dd1ba 补充提交 7 месяцев назад
  liangwenxuan a08b21b607 新增 7 месяцев назад
  liangwenxuan 6e180625fc 新增 7 месяцев назад
  liangwenxuan 1419f0ee51 新增 7 месяцев назад
  liangwenxuan 659b4ad822 修改 7 месяцев назад
  liangwenxuan d8fbe61ee7 新增 7 месяцев назад
  liangwenxuan b269f42d68 新增 7 месяцев назад
100 измененных файлов с 4331 добавлено и 558 удалено
  1. 4 0
      sql/mysql/alter_elderly_ask_leave_add_scaner.sql
  2. 30 0
      sql/mysql/alter_elderly_blood_sugar_record_add_time_period.sql
  3. 30 0
      sql/mysql/alter_elderly_check_in_wait_add_medical_report_urls.sql
  4. 3 0
      sql/mysql/alter_elderly_discharge_abstract_add_discharge_date.sql
  5. 4 0
      sql/mysql/alter_elderly_outbound_refund_config_add_is_auto_cross.sql
  6. 39 0
      sql/mysql/appointment_config.sql
  7. 17 0
      sql/mysql/elderly_build_floor_order_food.sql
  8. 47 0
      sql/mysql/elderly_discharge_abstract.sql
  9. 106 0
      sql/mysql/elderly_doctor_order.sql
  10. 30 0
      sql/mysql/food_satisfaction_survey.sql
  11. 22 0
      sql/mysql/visit_appointment.sql
  12. 11 0
      yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/api/task/BpmProcessInstanceApi.java
  13. 51 0
      yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/api/task/dto/BpmTaskRespDTO.java
  14. 14 0
      yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/api/task/dto/UserSimpleBaseDTO.java
  15. 1 0
      yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/ErrorCodeConstants.java
  16. 4 0
      yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/message/BpmMessageEnum.java
  17. 140 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/api/task/BpmProcessInstanceApiImpl.java
  18. 44 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/business/BpmCheckInWaitController.java
  19. 34 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/business/BpmCheckInWaitQueryController.java
  20. 26 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/business/BpmElderlyController.java
  21. 2 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/business/vo/BedChangeRecordCreateReqVO.java
  22. 36 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/business/vo/BpmApplyRetreatSimpleCreateReqVO.java
  23. 70 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/business/vo/BpmCheckInWaitCreateReqVO.java
  24. 28 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/business/vo/BpmCheckInWaitSupplementReqVO.java
  25. 78 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/business/vo/CateringChangeBatchCreateReqVO.java
  26. 3 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/business/vo/CateringChangeRecordCreateReqVO.java
  27. 2 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/business/vo/NurseChangeRecordCreateReqVO.java
  28. 2 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/business/vo/PriceChangeRecordCreateReqVO.java
  29. 2 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmFormController.java
  30. 116 3
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmProcessInstanceController.java
  31. 24 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/elderly/CheckInWaitPageReqVO.java
  32. 59 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/excel/BedChangeRecordExcelVO.java
  33. 66 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/excel/CateringChangeRecordExcelVO.java
  34. 69 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/excel/CheckInRecordExcelVO.java
  35. 75 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/excel/CheckInWaitRecordExcelVO.java
  36. 59 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/excel/NurseChangeRecordExcelVO.java
  37. 42 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/excel/PriceChangeRecordExcelVO.java
  38. 20 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmProcessOverdueReqVO.java
  39. 34 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmProcessOverdueRespVO.java
  40. 3 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskPageReqVO.java
  41. 1 1
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/definition/BpmModelConvert.java
  42. 50 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/excel/BpmCheckInWaitStatusConvert.java
  43. 56 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/excel/BpmProcessInstanceStatusConvert.java
  44. 60 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/excel/CheckInStatusConvert.java
  45. 57 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/excel/FileStatusConvert.java
  46. 49 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/excel/LocalDateConvert.java
  47. 49 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/excel/LocalDateTimeConvert.java
  48. 45 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/excel/YesNoConvert.java
  49. 4 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/definition/BpmFormDO.java
  50. 21 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/definition/BpmTaskCopyRelationDO.java
  51. 1 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/mysql/definition/BpmFormMapper.java
  52. 9 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/mysql/definition/BpmTaskCopyRelationMapper.java
  53. 2 1
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/enums/BpmBusinessTypeEnum.java
  54. 46 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/enums/BpmCheckInWaitStatusEnum.java
  55. 3 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmProcessInstanceEventListener.java
  56. 12 7
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/business/BpmBusinessFormServiceImpl.java
  57. 18 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/business/BpmElderlApplyService.java
  58. 220 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/business/BpmElderlApplyServiceImpl.java
  59. 2 2
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/business/ModelConfigServiceImpl.java
  60. 45 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/business/listener/BpmCheckInWaitStatusListener.java
  61. 49 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/business/listener/BpmOutboundRegistrationStatusListener.java
  62. 2 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmFormServiceImpl.java
  63. 101 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/message/BpmMessageServiceImpl.java
  64. 14 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceService.java
  65. 121 1
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java
  66. 233 4
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java
  67. 17 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/resources/mapper/BpmBusinessFormMapper.xml
  68. 9 1
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileController.java
  69. 198 37
      yudao-module-report/yudao-module-report-biz/src/main/java/cn/iocoder/yudao/module/report/controller/admin/goview/GoViewDataController.java
  70. 6 0
      yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/bpm/BpmAskLeaveApi.java
  71. 2 0
      yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/bpm/BpmCheckInApi.java
  72. 36 0
      yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/bpm/BpmCheckInWaitApi.java
  73. 2 0
      yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/bpm/BpmElderlyApi.java
  74. 3 0
      yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/bpm/BpmElderlyChangeApi.java
  75. 28 1
      yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/bpm/BpmElderlyExpenseApi.java
  76. 1 0
      yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/bpm/vo/CheckInExpenseItemReqVo.java
  77. 32 0
      yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/bpm/vo/CheckInWaitApiPageReqVO.java
  78. 38 0
      yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/bpm/vo/CheckInWaitCreateReqVO.java
  79. 45 0
      yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/bpm/vo/CheckInWaitRecordRespVO.java
  80. 39 0
      yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/bpm/vo/CheckInWaitSupplementReqVO.java
  81. 1 0
      yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/bpm/vo/ElderlyChangeRecordReqVO.java
  82. 31 0
      yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/bpm/vo/ElderlyCurrentChargeItemRespVO.java
  83. 0 3
      yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/kingdee/KingdeeApi.java
  84. 4 0
      yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/kingdee/dto/ReceiveBillSaveEntryDTO.java
  85. 1 0
      yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/kingdee/dto/ReceiveBillSaveModelDTO.java
  86. 2 0
      yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/notify/NotifyMessageSendApi.java
  87. 10 1
      yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/order/DTO/OrderItemRespDTO.java
  88. 22 2
      yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/order/OrderApi.java
  89. 10 1
      yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/order/VO/OrderItemRespVO.java
  90. 18 0
      yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/sys/BusinessParamsConfigApi.java
  91. 69 0
      yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java
  92. 2 1
      yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/change/BusinessTypeEnum.java
  93. 2 0
      yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/change/ChangeConstants.java
  94. 25 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/bpm/BpmAskLeaveApiImpl.java
  95. 120 65
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/bpm/BpmCheckInApiImpl.java
  96. 177 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/bpm/BpmCheckInWaitApiImpl.java
  97. 57 2
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/bpm/BpmElderlyApiImpl.java
  98. 148 45
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/bpm/BpmElderlyChangeApiImpl.java
  99. 269 87
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/bpm/BpmElderlyExpenseApiImpl.java
  100. 190 293
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/kingdee/KingdeeApiImpl.java

+ 4 - 0
sql/mysql/alter_elderly_ask_leave_add_scaner.sql

@@ -0,0 +1,4 @@
+-- 老人请假表新增扫码人字段
+-- 记录出院/回院扫码操作人(取自当前登录用户昵称)
+ALTER TABLE `elderly_ask_leave`
+    ADD COLUMN `scaner` varchar(100) NOT NULL DEFAULT '' COMMENT '扫码人(出院/回院扫码操作人)' AFTER `request_type`;

+ 30 - 0
sql/mysql/alter_elderly_blood_sugar_record_add_time_period.sql

@@ -0,0 +1,30 @@
+-- ============================================================
+-- 长者血糖记录改造:新增各时段血糖值及护士签名字段
+-- 原 blood_sugar 保留,作为「自定义时间点」的血糖记录值
+-- ============================================================
+ALTER TABLE `elderly_blood_sugar_record`
+  -- 1、空腹
+  ADD COLUMN `fasting_blood_sugar` decimal(6, 2) NULL DEFAULT NULL COMMENT '空腹血糖值(mmol/L)' AFTER `record_date`,
+  ADD COLUMN `fasting_nurse_sign` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '空腹护士签名' AFTER `fasting_blood_sugar`,
+
+  -- 2、早餐后
+  ADD COLUMN `after_breakfast_blood_sugar` decimal(6, 2) NULL DEFAULT NULL COMMENT '早餐后血糖值(mmol/L)' AFTER `fasting_nurse_sign`,
+  ADD COLUMN `after_breakfast_nurse_sign` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '早餐后护士签名' AFTER `after_breakfast_blood_sugar`,
+
+  -- 3、晚餐前
+  ADD COLUMN `before_dinner_blood_sugar` decimal(6, 2) NULL DEFAULT NULL COMMENT '晚餐前血糖值(mmol/L)' AFTER `after_breakfast_nurse_sign`,
+  ADD COLUMN `before_dinner_nurse_sign` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '晚餐前护士签名' AFTER `before_dinner_blood_sugar`,
+
+  -- 4、晚餐后
+  ADD COLUMN `after_dinner_blood_sugar` decimal(6, 2) NULL DEFAULT NULL COMMENT '晚餐后血糖值(mmol/L)' AFTER `before_dinner_nurse_sign`,
+  ADD COLUMN `after_dinner_nurse_sign` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '晚餐后护士签名' AFTER `after_dinner_blood_sugar`,
+
+  -- 5、睡前
+  ADD COLUMN `before_sleep_blood_sugar` decimal(6, 2) NULL DEFAULT NULL COMMENT '睡前血糖值(mmol/L)' AFTER `after_dinner_nurse_sign`,
+  ADD COLUMN `before_sleep_nurse_sign` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '睡前护士签名' AFTER `before_sleep_blood_sugar`,
+
+  -- 6、自定义时间点(原 blood_sugar 作为该时间点的血糖记录值)
+  ADD COLUMN `record_time` varchar(5) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '记录时间(HH:mm)' AFTER `before_sleep_nurse_sign`,
+  ADD COLUMN `nurse_sign` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '护士签名' AFTER `blood_sugar`,
+
+  ADD INDEX `idx_elder_date` (`elder_id`, `record_date`);

+ 30 - 0
sql/mysql/alter_elderly_check_in_wait_add_medical_report_urls.sql

@@ -0,0 +1,30 @@
+-- 入住待审表体检报告 url 字段(非必填)
+-- fourth_approve 阶段上传的各项体检报告(均为 url,允许为空),使用 TEXT 避免行大小超过 65535 字节上限
+-- 血生化(blood_biochemistry) 由 肾功能/血脂/心肌酶/电解质/凝血功能 合并而来
+
+-- 步骤一:若此前已按旧结构执行过本脚本,先删除已被合并的 5 个字段(首次执行请注释掉此段)
+ALTER TABLE `elderly_check_in_wait`
+    DROP COLUMN `renal_function`,
+    DROP COLUMN `blood_fat`,
+    DROP COLUMN `myocardial_enzyme`,
+    DROP COLUMN `electrolyte`,
+    DROP COLUMN `coagulation_function`;
+
+-- 步骤二:新增体检报告 url 字段
+ALTER TABLE `elderly_check_in_wait`
+    ADD COLUMN `second_liver_two_half_and_half` text DEFAULT NULL COMMENT '乙肝两对半' AFTER `commit_remark`,
+    ADD COLUMN `syphilis` text DEFAULT NULL COMMENT '梅毒' AFTER `second_liver_two_half_and_half`,
+    ADD COLUMN `hiv` text DEFAULT NULL COMMENT 'HIV' AFTER `syphilis`,
+    ADD COLUMN `tuberculosis_bacillus_antibody` text DEFAULT NULL COMMENT '结核杆菌抗体' AFTER `hiv`,
+    ADD COLUMN `hepatitis_c` text DEFAULT NULL COMMENT '丙肝' AFTER `tuberculosis_bacillus_antibody`,
+    ADD COLUMN `hepatitis_e` text DEFAULT NULL COMMENT '戊肝' AFTER `hepatitis_c`,
+    ADD COLUMN `blood_routine_examination` text DEFAULT NULL COMMENT '血常规' AFTER `hepatitis_e`,
+    ADD COLUMN `urine_routines` text DEFAULT NULL COMMENT '尿常规' AFTER `blood_routine_examination`,
+    ADD COLUMN `fecal_routine` text DEFAULT NULL COMMENT '粪常规' AFTER `urine_routines`,
+    ADD COLUMN `liver_function` text DEFAULT NULL COMMENT '肝功能' AFTER `fecal_routine`,
+    ADD COLUMN `blood_biochemistry` text DEFAULT NULL COMMENT '血生化' AFTER `liver_function`,
+    ADD COLUMN `rabat` text DEFAULT NULL COMMENT '胸片' AFTER `blood_biochemistry`,
+    ADD COLUMN `head_ct` text DEFAULT NULL COMMENT '头颅CT' AFTER `rabat`,
+    ADD COLUMN `ecg` text DEFAULT NULL COMMENT '心电图' AFTER `head_ct`,
+    ADD COLUMN `bmd` text DEFAULT NULL COMMENT '骨密度' AFTER `ecg`,
+    ADD COLUMN `cognitive_assessment_report` text DEFAULT NULL COMMENT '认知评估报告' AFTER `bmd`;

+ 3 - 0
sql/mysql/alter_elderly_discharge_abstract_add_discharge_date.sql

@@ -0,0 +1,3 @@
+-- 出院小结:新增出院日期字段
+ALTER TABLE `elderly_discharge_abstract`
+    ADD COLUMN `discharge_date` date NULL DEFAULT NULL COMMENT '出院日期' AFTER `elder_id`;

+ 4 - 0
sql/mysql/alter_elderly_outbound_refund_config_add_is_auto_cross.sql

@@ -0,0 +1,4 @@
+-- 外出退费配置表新增是否自动跨月字段
+-- 0:否(所有费用生成在归属月中),1:是(默认,跨月时按月份拆分)
+ALTER TABLE `elderly_outbound_refund_config`
+    ADD COLUMN `is_auto_cross` tinyint(1) NOT NULL DEFAULT 1 COMMENT '是否自动跨月,0:否,1:是' AFTER `config_type`;

+ 39 - 0
sql/mysql/appointment_config.sql

@@ -0,0 +1,39 @@
+DROP TABLE IF EXISTS `appointment_config`;
+CREATE TABLE `appointment_config`  (
+  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+  `enabled` tinyint(1) NOT NULL DEFAULT 0 COMMENT '预约总开关:0-关闭,1-开启',
+  `total_monthly_limit` int(11) NULL DEFAULT NULL COMMENT '总名额-每月限制(如100)',
+  `total_weekly_limit` int(11) NULL DEFAULT NULL COMMENT '总名额-每周限制(如50)',
+  `total_daily_limit` int(11) NULL DEFAULT NULL COMMENT '总名额-每日限制(如10)',
+  `person_monthly_limit` int(11) NULL DEFAULT NULL COMMENT '单人名额-每月限制(如100)',
+  `person_weekly_limit` int(11) NULL DEFAULT NULL COMMENT '单人名额-每周限制(如50)',
+  `person_daily_limit` int(11) NULL DEFAULT NULL COMMENT '单人名额-每日限制(如10)',
+  `begin_book_days` int(11) NULL DEFAULT 0 COMMENT '开始预约日(0表示当天)',
+  `advance_days` int(11) NULL DEFAULT 0 COMMENT '提前预约天数(0表示当天)',
+  `open_time_slot` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '开放时间段(逗号分隔,如"08:00-12:00,14:00-18:00",为空表示不限制)',
+  `create_time` datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `update_time` datetime NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+  `creator` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '创建人',
+  `updater` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '更新人',
+  `tenant_id` bigint(20) NULL DEFAULT NULL COMMENT '机构id',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '预约配置表' ROW_FORMAT = Dynamic;
+
+DROP TABLE IF EXISTS `appointment_group`;
+CREATE TABLE `appointment_group`  (
+  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+  `date_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '日期类型:DAILY-每天, WEEKLY-每周, MONTHLY-每月, CUSTOM-自定义',
+  `week_days` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '当date_type=WEEKLY时,存储周几,用逗号分隔,如"1,3,5"(1=周一)',
+  `month_days` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '当date_type=MONTHLY时,存储几号,如"1,15"(可包含负数表示倒数日)',
+  `custom_dates` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '当date_type=CUSTOM时,存储具体日期数组,如["2026-07-15","2026-07-20"]',
+  `limit_count` int(11) NULL DEFAULT NULL COMMENT '限制人数(null 表示不限制)',
+  `begin_time` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '可预约开始时间(HH:mm),null 表示不限',
+  `end_time` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '可预约结束时间(HH:mm),null 表示不限',
+  `create_time` datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `update_time` datetime NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+  `creator` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '创建人',
+  `updater` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '更新人',
+  `tenant_id` bigint(20) NULL DEFAULT NULL COMMENT '机构id',
+  `enable` tinyint(1) NOT NULL DEFAULT 1 COMMENT '使用状态:0-禁用,1-启用',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '预约组表' ROW_FORMAT = Dynamic;

+ 17 - 0
sql/mysql/elderly_build_floor_order_food.sql

@@ -0,0 +1,17 @@
+DROP TABLE IF EXISTS `elderly_build_floor_order_food`;
+CREATE TABLE `elderly_build_floor_order_food`  (
+  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
+  `build_id` int(11) NULL DEFAULT NULL COMMENT '楼栋id',
+  `build_name` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '楼栋名称',
+  `floor_id` int(11) NULL DEFAULT NULL COMMENT '楼层id',
+  `floor_name` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '楼层名称',
+  `order_date` date NULL DEFAULT NULL COMMENT '报餐日期',
+  `order_data_json` varchar(2048) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '报餐json数据',
+  `remark` varchar(1024) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注',
+  `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
+  `creator` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '创建人',
+  `update_time` datetime NULL DEFAULT NULL COMMENT '修改时间',
+  `updater` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '修改人',
+  `tenant_id` bigint(20) NULL DEFAULT NULL COMMENT '机构id',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '楼层报餐' ROW_FORMAT = Dynamic;

+ 47 - 0
sql/mysql/elderly_discharge_abstract.sql

@@ -0,0 +1,47 @@
+-- ============================================================
+-- 出院小结(主表:elderly_discharge_abstract / 子表:elderly_discharge_use_medicine)
+-- ============================================================
+
+DROP TABLE IF EXISTS `elderly_discharge_abstract`;
+CREATE TABLE `elderly_discharge_abstract`  (
+  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
+  `elder_id` bigint(20) NULL DEFAULT NULL COMMENT '长者id',
+  `discharge_date` date NULL DEFAULT NULL COMMENT '出院日期',
+  `anamnesis` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '既往病史',
+  `recent_health_issues` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '近期健康问题',
+  `physical_activity_ability` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '身体活动能力',
+  `cognition_mental_state` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '认知与精神状态',
+  `disease_treatment_control` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '疾病治疗与控制',
+  `recent_medical_treatment_situation` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '近期就医情况',
+  `reason_discharge` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '出院原因',
+  `vital_signs` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '生命体征',
+  `physical_functions` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '身体功能',
+  `key_issues` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '重点关注问题',
+  `safety_protection` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '安全防护',
+  `diet_medical_treatment` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '饮食与就医',
+  `doctor_sign` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '医生签名',
+  `sign_date` date NULL DEFAULT NULL COMMENT '签名日期',
+  `tenant_id` bigint(20) NULL DEFAULT NULL COMMENT '机构id',
+  `update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
+  `updater` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '更新人',
+  `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
+  `creator` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '创建人',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '出院小结' ROW_FORMAT = Dynamic;
+
+DROP TABLE IF EXISTS `elderly_discharge_use_medicine`;
+CREATE TABLE `elderly_discharge_use_medicine`  (
+  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'id',
+  `elder_id` int(11) NULL DEFAULT NULL COMMENT '长者id',
+  `discharge_abstract_id` int(11) NULL DEFAULT NULL COMMENT '出院小结id',
+  `medicine_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '药品',
+  `dose` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '剂量',
+  `usage` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '用法',
+  `frequency` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '频次',
+  `tenant_id` bigint(20) NULL DEFAULT NULL COMMENT '机构id',
+  `update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
+  `updater` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '更新人',
+  `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
+  `creator` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '创建人',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '出院小结用药' ROW_FORMAT = Dynamic;

+ 106 - 0
sql/mysql/elderly_doctor_order.sql

@@ -0,0 +1,106 @@
+-- ============================================================
+-- 医嘱模块(长期医嘱 / 临时医嘱 / 护士执行记录)
+-- 说明:业务表统一使用 elderly_ 前缀,与本项目其他业务表保持一致。
+-- 频次(frequency)/给药时间(medTime)/途径(route) 均以缩写存储,后端不翻译。
+-- ============================================================
+
+CREATE TABLE elderly_long_doctor_order (
+  id               BIGINT       NOT NULL AUTO_INCREMENT,
+  elder_id         BIGINT       NOT NULL,
+  elder_name       VARCHAR(64)  DEFAULT '',
+  gender           TINYINT      DEFAULT NULL,
+  gender_name      VARCHAR(16)  DEFAULT '',
+  bed_name         VARCHAR(64)  DEFAULT '',
+  nurse_level_name VARCHAR(64)  DEFAULT '',
+  tenant_id        BIGINT       DEFAULT NULL,
+  creator          VARCHAR(64)  DEFAULT '' COMMENT '创建者',
+  create_time      DATETIME     DEFAULT NULL COMMENT '创建时间',
+  updater          VARCHAR(64)  DEFAULT '' COMMENT '更新者',
+  update_time      DATETIME     DEFAULT NULL COMMENT '更新时间',
+  PRIMARY KEY (id)
+) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '长期医嘱主单';
+
+CREATE TABLE elderly_long_doctor_order_entry (
+  id                BIGINT       NOT NULL AUTO_INCREMENT,
+  long_doctor_order_id BIGINT    NOT NULL,
+  order_time        DATETIME     DEFAULT NULL COMMENT '开医嘱时间',
+  order_content     VARCHAR(512) DEFAULT '' COMMENT '医嘱内容',
+  frequency         VARCHAR(20)  DEFAULT '' COMMENT '给药频次缩写',
+  med_time          VARCHAR(20)  DEFAULT '' COMMENT '给药时间缩写',
+  route             VARCHAR(20)  DEFAULT '' COMMENT '给药途径缩写',
+  doctor_sign       VARCHAR(64)  DEFAULT '' COMMENT '医生签名',
+  nurse_sign        VARCHAR(64)  DEFAULT '' COMMENT '护士签名',
+  stop_time         DATETIME     DEFAULT NULL COMMENT '停止时间',
+  stop_doctor_sign  VARCHAR(64)  DEFAULT '' COMMENT '停止医生签名',
+  stop_nurse_sign   VARCHAR(64)  DEFAULT '' COMMENT '停止护士签名',
+  remark            VARCHAR(500) DEFAULT '' COMMENT '备注',
+  status            TINYINT      NOT NULL DEFAULT 0 COMMENT '0未开始 1在用 2已停止',
+  creator          VARCHAR(64)  DEFAULT '' COMMENT '创建者',
+  create_time      DATETIME     DEFAULT NULL COMMENT '创建时间',
+  updater          VARCHAR(64)  DEFAULT '' COMMENT '更新者',
+  update_time      DATETIME     DEFAULT NULL COMMENT '更新时间',
+  PRIMARY KEY (id),
+  KEY idx_long_order (long_doctor_order_id)
+) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '长期医嘱条目';
+
+CREATE TABLE elderly_short_doctor_order (
+  id               BIGINT       NOT NULL AUTO_INCREMENT,
+  elder_id         BIGINT       NOT NULL,
+  elder_name       VARCHAR(64)  DEFAULT '',
+  gender           TINYINT      DEFAULT NULL,
+  gender_name      VARCHAR(16)  DEFAULT '',
+  bed_name         VARCHAR(64)  DEFAULT '',
+  nurse_level_name VARCHAR(64)  DEFAULT '',
+  tenant_id        BIGINT       DEFAULT NULL,
+  creator          VARCHAR(64)  DEFAULT '' COMMENT '创建者',
+  create_time      DATETIME     DEFAULT NULL COMMENT '创建时间',
+  updater          VARCHAR(64)  DEFAULT '' COMMENT '更新者',
+  update_time      DATETIME     DEFAULT NULL COMMENT '更新时间',
+  PRIMARY KEY (id)
+) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '临时医嘱主单';
+
+CREATE TABLE elderly_short_doctor_order_entry (
+  id                BIGINT       NOT NULL AUTO_INCREMENT,
+  short_doctor_order_id BIGINT   NOT NULL,
+  order_time        DATETIME     DEFAULT NULL COMMENT '开医嘱时间',
+  order_content     VARCHAR(512) DEFAULT '' COMMENT '医嘱内容',
+  frequency         VARCHAR(20)  DEFAULT '' COMMENT '给药频次缩写',
+  med_time          VARCHAR(20)  DEFAULT '' COMMENT '给药时间缩写',
+  route             VARCHAR(20)  DEFAULT '' COMMENT '给药途径缩写',
+  doctor_sign       VARCHAR(64)  DEFAULT '' COMMENT '医生签名',
+  execute_time      DATETIME     DEFAULT NULL COMMENT '执行时间(临时医嘱即时执行)',
+  nurse_sign        VARCHAR(64)  DEFAULT '' COMMENT '护士签名',
+  remark            VARCHAR(500) DEFAULT '' COMMENT '备注',
+  creator          VARCHAR(64)  DEFAULT '' COMMENT '创建者',
+  create_time      DATETIME     DEFAULT NULL COMMENT '创建时间',
+  updater          VARCHAR(64)  DEFAULT '' COMMENT '更新者',
+  update_time      DATETIME     DEFAULT NULL COMMENT '更新时间',
+  PRIMARY KEY (id),
+  KEY idx_short_order (short_doctor_order_id)
+) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '临时医嘱条目';
+
+CREATE TABLE elderly_doctor_order_execution (
+  id              BIGINT       NOT NULL AUTO_INCREMENT,
+  order_type      TINYINT      NOT NULL COMMENT '1=长期医嘱 2=临时医嘱',
+  order_entry_id  BIGINT       NOT NULL COMMENT '关联 long/short_doctor_order_entry.id',
+  elder_id        BIGINT       DEFAULT NULL,
+  elder_name      VARCHAR(64)  DEFAULT '',
+  bed_name        VARCHAR(64)  DEFAULT '',
+  order_content   VARCHAR(512) DEFAULT '' COMMENT '医嘱内容冗余,便于打印',
+  frequency       VARCHAR(20)  DEFAULT '' COMMENT '频次缩写',
+  med_time        VARCHAR(20)  DEFAULT '' COMMENT '给药时间缩写',
+  route           VARCHAR(20)  DEFAULT '' COMMENT '给药途径缩写',
+  plan_time       DATETIME     DEFAULT NULL COMMENT '计划执行时间,前端生成 YYYY-MM-DD HH:mm',
+  exec_time       DATETIME     DEFAULT NULL COMMENT '实际执行时间',
+  exec_nurse_sign VARCHAR(64)  DEFAULT '' COMMENT '执行护士签名',
+  status          TINYINT      NOT NULL DEFAULT 0 COMMENT '0待执行 1已执行 2已跳过',
+  remark          VARCHAR(500) DEFAULT '' COMMENT '执行备注 / 跳过原因',
+  tenant_id       BIGINT       DEFAULT NULL,
+  creator          VARCHAR(64)  DEFAULT '' COMMENT '创建者',
+  create_time      DATETIME     DEFAULT NULL COMMENT '创建时间',
+  updater          VARCHAR(64)  DEFAULT '' COMMENT '更新者',
+  update_time      DATETIME     DEFAULT NULL COMMENT '更新时间',
+  PRIMARY KEY (id),
+  UNIQUE KEY uk_exec (order_type, order_entry_id, plan_time),
+  KEY idx_plan (plan_time, tenant_id)
+) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '护士执行记录';

+ 30 - 0
sql/mysql/food_satisfaction_survey.sql

@@ -0,0 +1,30 @@
+DROP TABLE IF EXISTS `food_satisfaction_survey`;
+CREATE TABLE `food_satisfaction_survey` (
+  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
+  `role` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '角色',
+  `record_date` date DEFAULT NULL COMMENT '记录日期',
+  `fresh` decimal(5, 2) DEFAULT NULL COMMENT '菜品新鲜度',
+  `taste` decimal(5, 2) DEFAULT NULL COMMENT '菜品口味(咸淡、口感等)',
+  `variety` decimal(5, 2) DEFAULT NULL COMMENT '菜品种类丰富度',
+  `staple` decimal(5, 2) DEFAULT NULL COMMENT '主食多样性(米饭、面食等)',
+  `soup` decimal(5, 2) DEFAULT NULL COMMENT '汤品 / 饮品口感与温度',
+  `nutrtion` decimal(5, 2) DEFAULT NULL COMMENT '菜品营养搭配合理性',
+  `tableware` decimal(5, 2) DEFAULT NULL COMMENT '餐具清洁程度',
+  `environment` decimal(5, 2) DEFAULT NULL COMMENT '用餐环境卫生(桌椅、地面等)',
+  `attitude` decimal(5, 2) DEFAULT NULL COMMENT '工作人员服务态度',
+  `timely` decimal(5, 2) DEFAULT NULL COMMENT '菜品供应及时性(未出现长时间断供)',
+  `portion` decimal(5, 2) DEFAULT NULL COMMENT '菜品分量充足度',
+  `soft_food` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '是否满足软食需求',
+  `vegetarian` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '是否满足素食需求',
+  `allergy_label` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '对有过敏食材的人群,是否能提供明确的食材标注',
+  `improve` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '最需要改进的方面',
+  `new_dishes` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '希望新增哪些菜品或服务',
+  `other` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '其他想反馈的内容',
+  `quality_total` decimal(5, 2) DEFAULT NULL COMMENT '膳食质量评价总分',
+  `service_total` decimal(5, 2) DEFAULT NULL COMMENT '膳食服务评价总分',
+  `submitter` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '提交人',
+  `tenant_id` bigint(20) DEFAULT NULL COMMENT '机构id',
+  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
+  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci COMMENT='膳食满意度调查' ROW_FORMAT=Dynamic;

+ 22 - 0
sql/mysql/visit_appointment.sql

@@ -0,0 +1,22 @@
+DROP TABLE IF EXISTS `visit_appointment`;
+CREATE TABLE `visit_appointment`  (
+  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '预约ID',
+  `order_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '预约订单号',
+  `org_id` int(10) UNSIGNED ZEROFILL NOT NULL COMMENT '养老机构名称',
+  `org_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
+  `elder_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '长者姓名',
+  `elder_room` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '长者房间号',
+  `visitor_open_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
+  `visitor_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '预约人姓名',
+  `visitor_phone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '预约人手机号',
+  `relation` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '与长者关系',
+  `accompany_num` tinyint(3) UNSIGNED NOT NULL DEFAULT 0 COMMENT '随行人数',
+  `note` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '预约备注',
+  `visit_date` date NOT NULL COMMENT '预约日期',
+  `time_slot` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '预约时间段',
+  `status` tinyint(4) NOT NULL DEFAULT 0 COMMENT '状态:0-待确认,1-已预约,2-已取消,3-已完成,4-已删除',
+  `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+  PRIMARY KEY (`id`) USING BTREE,
+  INDEX `idx_visitor_open_id`(`visitor_open_id`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 12172 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '探访预约记录表' ROW_FORMAT = DYNAMIC;

+ 11 - 0
yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/api/task/BpmProcessInstanceApi.java

@@ -1,6 +1,7 @@
 package cn.iocoder.yudao.module.bpm.api.task;
 
 import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO;
+import cn.iocoder.yudao.module.bpm.api.task.dto.BpmTaskRespDTO;
 
 import javax.validation.Valid;
 import java.util.List;
@@ -25,6 +26,8 @@ public interface BpmProcessInstanceApi {
 
     String getProcessInstanceId(String businessKey);
 
+    Boolean deleteProcessInstanceCascade(Long userId, String processInstanceId, String reason);
+
     Boolean timedClosureProcess();
 
     Boolean processInstanceComplete();
@@ -32,4 +35,12 @@ public interface BpmProcessInstanceApi {
 
     Integer insertBpmBusinessFormDO(Long businessId, Long businessId2, Long tenantId);
 
+    /**
+     * 基于流程实例的 id,获取审批任务的信息(参考 BpmTaskController#getTaskListByProcessInstanceId)
+     *
+     * @param processInstanceId 流程实例编号
+     * @return 审批任务列表
+     */
+    List<BpmTaskRespDTO> getTaskListByProcessInstanceId(String processInstanceId);
+
 }

+ 51 - 0
yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/api/task/dto/BpmTaskRespDTO.java

@@ -0,0 +1,51 @@
+package cn.iocoder.yudao.module.bpm.api.task.dto;
+
+import lombok.Data;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Map;
+
+@Data
+public class BpmTaskRespDTO {
+
+    private String id;
+
+    private String name;
+
+    private LocalDateTime createTime;
+
+    private LocalDateTime endTime;
+
+    private Long durationInMillis;
+
+    private Integer status; // 参见 BpmTaskStatusEnum 枚举
+
+    private String reason;
+
+    private Long owner;
+    /**
+     * 负责人的用户信息
+     */
+    private UserSimpleBaseDTO ownerUser;
+
+    private Long assignee;
+    /**
+     * 审核的用户信息
+     */
+    private UserSimpleBaseDTO assigneeUser;
+
+    private String taskDefinitionKey;
+
+    private String processInstanceId;
+
+    private String parentTaskId;
+    private List<BpmTaskRespDTO> children; // 由加签生成,包含多层子任务
+
+    private Long formId;
+    private String formName;
+    private String formConf;
+    private List<String> formFields;
+    private Map<String, Object> formVariables;
+
+}

+ 14 - 0
yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/api/task/dto/UserSimpleBaseDTO.java

@@ -0,0 +1,14 @@
+package cn.iocoder.yudao.module.bpm.api.task.dto;
+
+import lombok.Data;
+
+@Data
+public class UserSimpleBaseDTO {
+
+    private Long id;
+    private String nickname;
+
+    private Long deptId;
+    private String deptName;
+
+}

+ 1 - 0
yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/ErrorCodeConstants.java

@@ -100,4 +100,5 @@ public interface ErrorCodeConstants {
     // ========== 校验规则 1-002-001-000 ==========
     ErrorCode APPLY_RETREAT_CURRENT_EXPENSE_ORDER_NOT_EXISTS = new ErrorCode(1_002_001_001, "费用截止日期月无该长者账单,请先生成!");
     ErrorCode APPLY_RETREAT_ASK_LEAVE_EXISTS = new ErrorCode(1_002_001_002, "长者存在未返院登记记录");
+    ErrorCode APPLY_RETREAT_TEMP_OUT_EXISTS = new ErrorCode(1_002_001_003, "长者存在未返院的离住记录,不能申请退住");
 }

+ 4 - 0
yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/message/BpmMessageEnum.java

@@ -50,6 +50,10 @@ public enum BpmMessageEnum {
     CHECK_IN_PROCESS_INSTANCE_APPROVE("check_in_process_instance_approve"),
     CHECK_IN_PROCESS_INSTANCE_REJECT("check_in_process_instance_reject"),
 
+    CHECK_IN_WAIT_PROCESS_TASK_ASSIGNED("check_in_wait_process_task_assigned"),
+    CHECK_IN_WAIT_PROCESS_INSTANCE_APPROVE("check_in_wait_process_instance_approve"),
+    CHECK_IN_WAIT_PROCESS_INSTANCE_REJECT("check_in_wait_process_instance_reject"),
+
     PROCESS_INSTANCE_BACK("process_instance_back"),
 
     CHECK_IN_HANDLE("check_in_handle"),

+ 140 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/api/task/BpmProcessInstanceApiImpl.java

@@ -1,14 +1,31 @@
 package cn.iocoder.yudao.module.bpm.api.task;
 
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.map.MapUtil;
+import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
+import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
 import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO;
+import cn.iocoder.yudao.module.bpm.api.task.dto.BpmTaskRespDTO;
+import cn.iocoder.yudao.module.bpm.api.task.dto.UserSimpleBaseDTO;
 import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceCancelReqVO;
 import cn.iocoder.yudao.module.bpm.dal.dataobject.business.BpmBusinessFormDO;
+import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmFormDO;
 import cn.iocoder.yudao.module.bpm.dal.mysql.business.BpmBusinessFormMapper;
+import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceStatusEnum;
+import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
+import cn.iocoder.yudao.module.bpm.service.definition.BpmFormService;
+import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceCopyService;
 import cn.iocoder.yudao.module.bpm.service.business.BpmBusinessFormService;
 import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService;
+import cn.iocoder.yudao.module.bpm.service.task.BpmTaskService;
+import cn.iocoder.yudao.module.system.api.dept.DeptApi;
+import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
+import cn.iocoder.yudao.module.system.api.notify.NotifyMessageSendApi;
+import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
+import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
 import cn.iocoder.yudao.module.system.api.bpm.BpmCheckInApi;
 import cn.iocoder.yudao.module.system.api.bpm.BpmElderlyApi;
 import cn.iocoder.yudao.module.system.api.bpm.BpmElderlyChangeApi;
@@ -16,6 +33,7 @@ import cn.iocoder.yudao.module.system.api.bpm.BpmElderlyExpenseApi;
 import cn.iocoder.yudao.module.system.api.bpm.vo.ElderlyChangeRecordReqVO;
 import cn.iocoder.yudao.module.system.api.bpm.vo.ElderlyRetreatRecordRespVO;
 import cn.iocoder.yudao.module.system.enums.change.BusinessTypeEnum;
+import org.flowable.engine.RuntimeService;
 import org.flowable.engine.HistoryService;
 import org.flowable.engine.TaskService;
 import org.flowable.engine.history.HistoricProcessInstance;
@@ -33,7 +51,11 @@ import java.time.LocalDate;
 import java.time.temporal.TemporalAdjusters;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Map;
+import java.util.stream.Stream;
 
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSetByFlatMap;
 import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
 
 /**
@@ -61,6 +83,27 @@ public class BpmProcessInstanceApiImpl implements BpmProcessInstanceApi {
     @Resource
     private BpmBusinessFormMapper businessFormMapper;
 
+    @Resource
+    private RuntimeService runtimeService;
+
+    @Resource
+    private BpmProcessInstanceCopyService processInstanceCopyService;
+
+    @Resource
+    private NotifyMessageSendApi notifyMessageSendApi;
+
+    @Resource
+    private BpmTaskService bpmTaskService;
+
+    @Resource
+    private BpmFormService formService;
+
+    @Resource
+    private AdminUserApi adminUserApi;
+
+    @Resource
+    private DeptApi deptApi;
+
     @Resource
     private BpmElderlyChangeApi elderlyChangeApi;
 
@@ -102,6 +145,26 @@ public class BpmProcessInstanceApiImpl implements BpmProcessInstanceApi {
         return processInstanceId;
     }
 
+    @Override
+    public Boolean deleteProcessInstanceCascade(Long userId, String processInstanceId, String reason) {
+        if (processInstanceId == null || processInstanceId.isEmpty()) {
+            return Boolean.FALSE;
+        }
+        String deleteReason = reason == null ? "" : reason;
+        if (processInstanceService.getProcessInstance(processInstanceId) != null) {
+            runtimeService.setVariable(processInstanceId, BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS,
+                    BpmProcessInstanceStatusEnum.CANCEL.getStatus());
+            runtimeService.setVariable(processInstanceId, BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_REASON, deleteReason);
+            runtimeService.deleteProcessInstance(processInstanceId, deleteReason);
+        }
+        processInstanceCopyService.deleteProcessInstanceCopy(processInstanceId);
+        notifyMessageSendApi.deleteByPkMessage(processInstanceId);
+        businessFormMapper.delete(new LambdaQueryWrapperX<BpmBusinessFormDO>()
+                .eq(BpmBusinessFormDO::getProcessInstanceId, processInstanceId));
+        historyService.deleteHistoricProcessInstance(processInstanceId);
+        return Boolean.TRUE;
+    }
+
     @Override
     public Boolean timedClosureProcess() {
 //        LocalDate nowLocalDate = LocalDate.now();
@@ -212,4 +275,81 @@ public class BpmProcessInstanceApiImpl implements BpmProcessInstanceApi {
         businessFormDO.setType(1);
         return businessFormMapper.insert(businessFormDO);
     }
+
+    @Override
+    public List<BpmTaskRespDTO> getTaskListByProcessInstanceId(String processInstanceId) {
+        if (processInstanceId == null || processInstanceId.isEmpty()) {
+            return new ArrayList<>();
+        }
+        List<HistoricTaskInstance> taskList = bpmTaskService.getTaskListByProcessInstanceId(processInstanceId, true);
+        if (CollUtil.isEmpty(taskList)) {
+            return new ArrayList<>();
+        }
+
+        // 拼接用户信息、部门信息
+        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
+                convertSetByFlatMap(taskList, task ->
+                        Stream.of(NumberUtils.parseLong(task.getAssignee()), NumberUtils.parseLong(task.getOwner()))));
+        Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(
+                convertSet(userMap.values(), AdminUserRespDTO::getDeptId));
+        // 获得 Form Map
+        Map<Long, BpmFormDO> formMap = formService.getFormMap(
+                convertSet(taskList, task -> NumberUtils.parseLong(task.getFormKey())));
+
+        return CollectionUtils.convertList(taskList, task -> {
+            // 已取消的任务,不返回
+            Integer taskStatus = FlowableUtils.getTaskStatus(task);
+            if (BpmTaskStatusEnum.isCancelStatus(taskStatus)) {
+                return null;
+            }
+            BpmTaskRespDTO taskDTO = new BpmTaskRespDTO();
+            taskDTO.setId(task.getId());
+            taskDTO.setName(task.getName());
+            taskDTO.setCreateTime(task.getCreateTime() == null ? null : task.getCreateTime().toInstant()
+                    .atZone(java.time.ZoneId.systemDefault()).toLocalDateTime());
+            taskDTO.setEndTime(task.getEndTime() == null ? null : task.getEndTime().toInstant()
+                    .atZone(java.time.ZoneId.systemDefault()).toLocalDateTime());
+            taskDTO.setDurationInMillis(task.getDurationInMillis());
+            taskDTO.setStatus(taskStatus);
+            taskDTO.setReason(FlowableUtils.getTaskReason(task));
+            taskDTO.setOwner(NumberUtils.parseLong(task.getOwner()));
+            taskDTO.setAssignee(NumberUtils.parseLong(task.getAssignee()));
+            taskDTO.setTaskDefinitionKey(task.getTaskDefinitionKey());
+            taskDTO.setProcessInstanceId(task.getProcessInstanceId());
+            taskDTO.setParentTaskId(task.getParentTaskId());
+            // 表单信息
+            BpmFormDO form = MapUtil.get(formMap, NumberUtils.parseLong(task.getFormKey()), BpmFormDO.class);
+            if (form != null) {
+                taskDTO.setFormId(form.getId());
+                taskDTO.setFormName(form.getName());
+                taskDTO.setFormConf(form.getConf());
+                taskDTO.setFormFields(form.getFields());
+                taskDTO.setFormVariables(FlowableUtils.getTaskFormVariable(task));
+            }
+            // 用户信息
+            taskDTO.setAssigneeUser(buildUserSimple(task.getAssignee(), userMap, deptMap));
+            taskDTO.setOwnerUser(buildUserSimple(task.getOwner(), userMap, deptMap));
+            return taskDTO;
+        });
+    }
+
+    private UserSimpleBaseDTO buildUserSimple(String userIdStr, Map<Long, AdminUserRespDTO> userMap, Map<Long, DeptRespDTO> deptMap) {
+        if (userIdStr == null) {
+            return null;
+        }
+        Long userId = NumberUtils.parseLong(userIdStr);
+        AdminUserRespDTO user = userMap.get(userId);
+        if (user == null) {
+            return null;
+        }
+        UserSimpleBaseDTO userDTO = new UserSimpleBaseDTO();
+        userDTO.setId(user.getId());
+        userDTO.setNickname(user.getNickname());
+        userDTO.setDeptId(user.getDeptId());
+        DeptRespDTO dept = deptMap.get(user.getDeptId());
+        if (dept != null) {
+            userDTO.setDeptName(dept.getName());
+        }
+        return userDTO;
+    }
 }

+ 44 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/business/BpmCheckInWaitController.java

@@ -0,0 +1,44 @@
+package cn.iocoder.yudao.module.bpm.controller.admin.business;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
+import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
+import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
+import cn.iocoder.yudao.module.bpm.controller.admin.business.vo.BpmCheckInWaitCreateReqVO;
+import cn.iocoder.yudao.module.bpm.controller.admin.business.vo.BpmCheckInWaitSupplementReqVO;
+import cn.iocoder.yudao.module.bpm.service.business.BpmElderlApplyService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+import javax.annotation.security.PermitAll;
+import javax.validation.Valid;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
+
+@Tag(name = "管理后台 - 长者轮候入住流程")
+@RestController
+@RequestMapping("/bpm/check-in-wait")
+@Validated
+public class BpmCheckInWaitController {
+
+    @Resource
+    private BpmElderlApplyService elderlApplyService;
+
+    @PostMapping("/create")
+    @Operation(summary = "创建轮候入住申请")
+    @TenantIgnore
+    @PermitAll
+    public CommonResult<Long> create(@Valid @RequestBody BpmCheckInWaitCreateReqVO createReqVO) {
+        createReqVO.setTenantId(createReqVO.getTenantId() == null ? TenantContextHolder.getTenantId() : createReqVO.getTenantId());
+        return success(elderlApplyService.checkInWaitCreate(getLoginUserId(),
+                SecurityFrameworkUtils.getLoginUserNickname(), createReqVO));
+    }
+}
+

+ 34 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/business/BpmCheckInWaitQueryController.java

@@ -0,0 +1,34 @@
+package cn.iocoder.yudao.module.bpm.controller.admin.business;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
+import cn.iocoder.yudao.module.system.api.bpm.BpmCheckInWaitApi;
+import com.alibaba.fastjson.JSONObject;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+
+@Tag(name = "管理后台 - 长者轮候入住流程")
+@RestController
+@RequestMapping("/bpm/check-in-wait")
+@Validated
+public class BpmCheckInWaitQueryController {
+
+    @Resource
+    private BpmCheckInWaitApi checkInWaitApi;
+
+    @GetMapping("/detail")
+    @Operation(summary = "获取轮候入住详情")
+    @TenantIgnore
+    public CommonResult<JSONObject> getCheckInWaitDetail(@RequestParam("id") Long id) {
+        return success(checkInWaitApi.selectById(id));
+    }
+}

+ 26 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/business/BpmElderlyController.java

@@ -17,6 +17,7 @@ import javax.annotation.Resource;
 import javax.annotation.security.PermitAll;
 import javax.validation.Valid;
 import cn.iocoder.yudao.module.bpm.service.business.BpmElderlApplyService;
+import java.util.Map;
 
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
 import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
@@ -55,6 +56,23 @@ public class BpmElderlyController {
         return success(elderlApplyService.createApplyRetreat(getLoginUserId(), createReqVO));
     }
 
+    @PostMapping("/retreatSimpleCreate")
+    @PreAuthorize("@ss.hasAnyPermissions('check-out:add', 'check-out:relaunch')")
+    @Operation(summary = "创建退住请求申请(离住转退住)")
+    @TenantIgnore
+    public CommonResult<Long> retreatSimpleCreate(@Valid @RequestBody BpmApplyRetreatSimpleCreateReqVO createReqVO) {
+        return success(elderlApplyService.createApplyRetreatSimple(getLoginUserId(), createReqVO));
+    }
+
+    @PostMapping("/retreatRevoke")
+    @PreAuthorize("@ss.hasAnyPermissions('check-out:add', 'check-out:relaunch')")
+    @Operation(summary = "撤销退住申请(审批通过未结算)")
+    @TenantIgnore
+    public CommonResult<Boolean> retreatRevoke(@RequestParam("businessId") Long retreatRecordId) {
+        elderlApplyService.revokeApprovedRetreat(getLoginUserId(), retreatRecordId);
+        return success(true);
+    }
+
     @PostMapping("/bedChangeCreate")
     @PreAuthorize("@ss.hasAnyPermissions('bed-change:add', 'bed-change:relaunch')")
     @Operation(summary = "创建床位变更")
@@ -79,6 +97,14 @@ public class BpmElderlyController {
         return success(elderlApplyService.cateringChangeCreate(getLoginUserId(), createReqVO));
     }
 
+    @PostMapping("/cateringChangeBatchCreate")
+    @PreAuthorize("@ss.hasAnyPermissions('changes:food:add', 'changes:food:relaunch')")
+    @Operation(summary = "批量创建餐饮变更")
+    @TenantIgnore
+    public CommonResult<Map<String, Object>> cateringChangeBatchCreate(@Valid @RequestBody CateringChangeBatchCreateReqVO reqVO) {
+        return success(elderlApplyService.cateringChangeBatchCreate(getLoginUserId(), reqVO));
+    }
+
     @PostMapping("/priceChangeCreate")
     @PreAuthorize("@ss.hasAnyPermissions('price-change:add', 'price-change:relaunch')")
     @Operation(summary = "创建价格变更")

+ 2 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/business/vo/BedChangeRecordCreateReqVO.java

@@ -121,5 +121,7 @@ public class BedChangeRecordCreateReqVO {
      * 原始金额
      */
     private BigDecimal originalAmount;
+    @Schema(description = "滞后至月份", example = "2026-01")
+    private String hysteresisMonth;
 
 }

+ 36 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/business/vo/BpmApplyRetreatSimpleCreateReqVO.java

@@ -0,0 +1,36 @@
+package cn.iocoder.yudao.module.bpm.controller.admin.business.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+import java.time.LocalDate;
+
+@Schema(description = "管理后台 - 退住申请(离住转退住) Request VO")
+@Data
+public class BpmApplyRetreatSimpleCreateReqVO {
+
+    @Schema(description = "退住日期", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "退住日期不能为空")
+    private LocalDate retreatDate;
+
+    @Schema(description = "费用截止日期", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "费用截止日期不能为空")
+    private LocalDate costDeadline;
+
+    @Schema(description = "长者ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    @NotNull(message = "长者ID不能为空")
+    private Long elderlyId;
+
+    @Schema(description = "退住原因", requiredMode = Schema.RequiredMode.REQUIRED, example = "不适应")
+    @NotEmpty(message = "退住原因不能为空")
+    private String retreatReason;
+
+    @Schema(description = "退住描述", example = "长者主动要求退住")
+    private String retreatDescribe;
+
+    @Schema(description = "离住记录ID(离住转退住时传入,用于对该离住记录做返院登记)", example = "1024")
+    private Long tempOutId;
+
+}

+ 70 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/business/vo/BpmCheckInWaitCreateReqVO.java

@@ -0,0 +1,70 @@
+package cn.iocoder.yudao.module.bpm.controller.admin.business.vo;
+
+import cn.iocoder.yudao.module.bpm.enums.BpmBusinessTypeEnum;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import java.util.List;
+import java.util.Map;
+
+@Schema(description = "管理后台 - 轮候入住发起 Request VO")
+@Data
+public class BpmCheckInWaitCreateReqVO {
+
+    @Schema(description = "主键ID")
+    private Long id;
+
+    @Schema(description = "长者姓名", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotBlank(message = "长者姓名不能为空")
+    private String elderName;
+
+    @Schema(description = "身份证")
+    @NotBlank(message = "身份证不能为空")
+    private String idCard;
+
+    @Schema(description = "院区ID", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "院区不能为空")
+    private Long tenantId;
+
+    @Schema(description = "近期生活状态视频", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotBlank(message = "近期生活状态不能为空")
+    private String recentLivingSituation;
+
+    @Schema(description = "行走情况视频", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotBlank(message = "行走情况视频不能为空")
+    private String walkSituationVideo;
+
+    @Schema(description = "半年内病历", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotBlank(message = "半年内病历不能为空")
+    private String medicalRecordsWithinSixMonth;
+
+    @Schema(description = "身份证正反面", requiredMode = Schema.RequiredMode.REQUIRED)
+    private String idCardPhoto;
+
+    @Schema(description = "近期出院小结")
+    private String recentDischargeSummary;
+
+    @Schema(description = "常用药")
+    private String commonMedicine;
+
+    @Schema(description = "申请类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
+    @NotNull(message = "申请类型不能为空")
+    private Integer type = BpmBusinessTypeEnum.CHECK_IN_WAIT.getValue();
+
+    @Schema(description = "业务ID")
+    private Long businessId;
+
+    @Schema(description = "业务ID2")
+    private Long businessId2;
+
+    @Schema(description = "流程实例编号")
+    private String processInstanceId;
+
+    @Schema(description = "发起人自选审批人")
+    private Map<String, List<Long>> startUserSelectAssignees;
+
+    @Schema(description = "提交备注")
+    private String commitRemark;
+}

+ 28 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/business/vo/BpmCheckInWaitSupplementReqVO.java

@@ -0,0 +1,28 @@
+package cn.iocoder.yudao.module.bpm.controller.admin.business.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+
+@Schema(description = "管理后台 - 轮候入住补充信息 Request VO")
+@Data
+public class BpmCheckInWaitSupplementReqVO {
+
+    @Schema(description = "轮候记录ID", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "轮候记录ID不能为空")
+    private Long id;
+
+    @Schema(description = "护理等级ID")
+    private Long nurseLevelId;
+
+    @Schema(description = "备注")
+    private String remark;
+
+    @Schema(description = "体检报告")
+    private String physicalExaminationReport;
+
+    @Schema(description = "床位ID")
+    private Long bedId;
+}
+

+ 78 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/business/vo/CateringChangeBatchCreateReqVO.java

@@ -0,0 +1,78 @@
+package cn.iocoder.yudao.module.bpm.controller.admin.business.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.List;
+
+@Schema(description = "管理后台 - 批量创建餐饮变更 Request VO")
+@Data
+public class CateringChangeBatchCreateReqVO {
+
+    @Schema(description = "院区/机构 tenantId", requiredMode = Schema.RequiredMode.REQUIRED, example = "209")
+    @NotNull(message = "tenantId不能为空")
+    private Long tenantId;
+
+    @Schema(description = "机构类型", example = "1")
+    private Integer orgType = 1;
+
+    @Schema(description = "变更生效日期", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-05-09")
+    @NotNull(message = "变更生效日期不能为空")
+    private LocalDate changeDate;
+
+    @Schema(description = "变更原因", requiredMode = Schema.RequiredMode.REQUIRED, example = "系统统一变更")
+    @NotEmpty(message = "变更原因不能为空")
+    private String reason;
+
+    @Schema(description = "收费项目名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "固定费用-餐饮费")
+    @NotEmpty(message = "收费项目名称不能为空")
+    private String categoryName;
+
+    @Schema(description = "变更文件(JSON字符串)", example = "[]")
+    private String changeFiles = "[]";
+
+    @Schema(description = "滞后至月份", example = "2026-01")
+    private String hysteresisMonth;
+
+    @Schema(description = "是否跳过重复申请(同长者同日期)", example = "true")
+    private Boolean skipRepeat = Boolean.TRUE;
+
+    @Schema(description = "是否沿用长者当前折扣信息", example = "true")
+    private Boolean keepDiscount = Boolean.TRUE;
+
+    @Schema(description = "匹配规则与目标餐饮项目列表", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotEmpty(message = "映射列表不能为空")
+    private List<ItemMapping> mappings;
+
+    @Data
+    public static class ItemMapping {
+
+        @Schema(description = "匹配的原费用项目id(优先使用)", example = "1803357950676461384")
+        private Long originalOverheadChargeId;
+
+        @Schema(description = "匹配的原费用项目名称(当 originalOverheadChargeId 为空时使用)", example = "普通食谱")
+        private String originalName;
+
+        @Schema(description = "匹配的原费用实际金额(当 originalOverheadChargeId 为空时使用)", example = "750")
+        private BigDecimal originalActualAmount;
+
+        @Schema(description = "新费用项目id", requiredMode = Schema.RequiredMode.REQUIRED, example = "1803357950676463024")
+        @NotNull(message = "新费用项目id不能为空")
+        private Long expectOverheadChargeId;
+
+        @Schema(description = "新费用项目名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "普通食谱")
+        @NotEmpty(message = "新费用项目名称不能为空")
+        private String expectName;
+
+        @Schema(description = "新费用金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "900")
+        @NotNull(message = "新费用金额不能为空")
+        private BigDecimal amount;
+
+        @Schema(description = "新费用实际金额(不传则按折扣规则自动计算)", example = "900")
+        private BigDecimal actualAmount;
+    }
+}

+ 3 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/business/vo/CateringChangeRecordCreateReqVO.java

@@ -111,4 +111,7 @@ public class CateringChangeRecordCreateReqVO {
      * 原始金额
      */
     private BigDecimal originalAmount;
+
+    @Schema(description = "滞后至月份", example = "2026-01")
+    private String hysteresisMonth;
 }

+ 2 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/business/vo/NurseChangeRecordCreateReqVO.java

@@ -117,5 +117,7 @@ public class NurseChangeRecordCreateReqVO {
      * 原始金额
      */
     private BigDecimal originalAmount;
+    @Schema(description = "滞后至月份", example = "2026-01")
+    private String hysteresisMonth;
 
 }

+ 2 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/business/vo/PriceChangeRecordCreateReqVO.java

@@ -69,5 +69,7 @@ public class  PriceChangeRecordCreateReqVO {
      * 原始金额
      */
     private BigDecimal originalAmount;
+    @Schema(description = "滞后至月份", example = "2026-01")
+    private String hysteresisMonth;
 
 }

+ 2 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmFormController.java

@@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.bpm.controller.admin.definition;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
 import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.form.BpmFormPageReqVO;
 import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.form.BpmFormRespVO;
 import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.form.BpmFormSaveReqVO;
@@ -66,6 +67,7 @@ public class BpmFormController {
 
     @GetMapping({"/list-all-simple", "/simple-list"})
     @Operation(summary = "获得动态表单的精简列表", description = "用于表单下拉框")
+    @TenantIgnore
     public CommonResult<List<BpmFormRespVO>> getFormSimpleList() {
         List<BpmFormDO> list = formService.getFormList();
         return success(convertList(list, formDO -> // 只返回 id、name 字段

+ 116 - 3
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmProcessInstanceController.java

@@ -5,9 +5,11 @@ import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
 import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
 import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
 import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
 import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
@@ -15,7 +17,9 @@ import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
 import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.elderly.BedChangePageReqVO;
 import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.elderly.BpmElderlyInfoVO;
 import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.elderly.CheckInPageReqVO;
+import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.elderly.CheckInWaitPageReqVO;
 import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.elderly.ElderlyRetreatRecordPageReqVO;
+import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.excel.*;
 import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.*;
 import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.BpmTaskPageReqVO;
 import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.BpmTaskRespVO;
@@ -48,11 +52,14 @@ import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 
 import javax.annotation.Resource;
+import javax.servlet.http.HttpServletResponse;
 import javax.validation.Valid;
+import java.io.IOException;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 
+import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT;
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
@@ -270,6 +277,21 @@ public class BpmProcessInstanceController {
         return success(BeanUtils.toBean(pageResult, BedChangeRecordRespVO.class));
     }
 
+    @GetMapping("/bed-change-export-excel")
+    @Operation(summary = "导出床位变更流程实例 Excel", description = "内容为床位变更分页的全量数据")
+    @ApiAccessLog(operateType = EXPORT)
+    @TenantIgnore
+    public void exportProcessInstanceBedChangeExcel(@Valid BedChangePageReqVO pageReqVO,
+                                                    HttpServletResponse response) throws IOException {
+        pageReqVO.setTenantIds(Objects.isNull(pageReqVO.getTenantIds()) ? new Long[]{TenantContextHolder.getTenantId()} : pageReqVO.getTenantIds());
+        pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
+        List<BedChangeRecordRespVO> list = processInstanceService.getProcessInstanceBedChangePage(
+                getLoginUserId(), pageReqVO).getList();
+        // 导出 Excel
+        ExcelUtils.write(response, "床位变更.xls", "数据", BedChangeRecordExcelVO.class,
+                BeanUtils.toBean(list, BedChangeRecordExcelVO.class));
+    }
+
     @GetMapping("/nurse-change-page")
     @Operation(summary = "获取护理变更流程实例", description = "在【我的流程】菜单中,进行调用")
 //    @PreAuthorize("@ss.hasPermission('bpm:process-instance:query')")
@@ -281,6 +303,21 @@ public class BpmProcessInstanceController {
         return success(pageResult);
     }
 
+    @GetMapping("/nurse-change-export-excel")
+    @Operation(summary = "导出护理变更流程实例 Excel", description = "内容为护理变更分页的全量数据")
+    @ApiAccessLog(operateType = EXPORT)
+    @TenantIgnore
+    public void exportProcessInstanceNurseChangeExcel(@Valid BedChangePageReqVO pageReqVO,
+                                                      HttpServletResponse response) throws IOException {
+        pageReqVO.setTenantIds(Objects.isNull(pageReqVO.getTenantIds()) ? new Long[]{TenantContextHolder.getTenantId()} : pageReqVO.getTenantIds());
+        pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
+        List<NurseChangeRecordRespVO> list = processInstanceService.getProcessInstanceNurseChangePage(
+                getLoginUserId(), pageReqVO).getList();
+        // 导出 Excel
+        ExcelUtils.write(response, "护理变更.xls", "数据", NurseChangeRecordExcelVO.class,
+                BeanUtils.toBean(list, NurseChangeRecordExcelVO.class));
+    }
+
     @GetMapping("/catering-change-page")
     @Operation(summary = "获取餐饮变更流程实例", description = "在【我的流程】菜单中,进行调用")
 //    @PreAuthorize("@ss.hasPermission('bpm:process-instance:query')")
@@ -292,6 +329,21 @@ public class BpmProcessInstanceController {
         return success(pageResult);
     }
 
+    @GetMapping("/catering-change-export-excel")
+    @Operation(summary = "导出餐饮变更流程实例 Excel", description = "内容为餐饮变更分页的全量数据")
+    @ApiAccessLog(operateType = EXPORT)
+    @TenantIgnore
+    public void exportProcessInstanceCateringChangeExcel(@Valid BedChangePageReqVO pageReqVO,
+                                                         HttpServletResponse response) throws IOException {
+        pageReqVO.setTenantIds(Objects.isNull(pageReqVO.getTenantIds()) ? new Long[]{TenantContextHolder.getTenantId()} : pageReqVO.getTenantIds());
+        pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
+        List<CateringChangeRecordRespVO> list = processInstanceService.getProcessInstanceCateringChangePage(
+                getLoginUserId(), pageReqVO).getList();
+        // 导出 Excel
+        ExcelUtils.write(response, "餐饮变更.xls", "数据", CateringChangeRecordExcelVO.class,
+                BeanUtils.toBean(list, CateringChangeRecordExcelVO.class));
+    }
+
     @GetMapping("/price-change-page")
     @Operation(summary = "获取价格变更流程实例", description = "在【我的流程】菜单中,进行调用")
 //    @PreAuthorize("@ss.hasPermission('bpm:process-instance:query')")
@@ -303,6 +355,21 @@ public class BpmProcessInstanceController {
         return success(pageResult);
     }
 
+    @GetMapping("/price-change-export-excel")
+    @Operation(summary = "导出价格变更流程实例 Excel", description = "内容为价格变更分页的全量数据")
+    @ApiAccessLog(operateType = EXPORT)
+    @TenantIgnore
+    public void exportProcessInstancePriceChangeExcel(@Valid BedChangePageReqVO pageReqVO,
+                                                      HttpServletResponse response) throws IOException {
+        pageReqVO.setTenantIds(Objects.isNull(pageReqVO.getTenantIds()) ? new Long[]{TenantContextHolder.getTenantId()} : pageReqVO.getTenantIds());
+        pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
+        List<PriceChangeRecordRespVO> list = processInstanceService.getProcessInstancePriceChangePage(
+                getLoginUserId(), pageReqVO).getList();
+        // 导出 Excel
+        ExcelUtils.write(response, "价格变更.xls", "数据", PriceChangeRecordExcelVO.class,
+                BeanUtils.toBean(list, PriceChangeRecordExcelVO.class));
+    }
+
     @GetMapping("/transfer-change-page")
     @Operation(summary = "获取转院变更流程实例", description = "在【我的流程】菜单中,进行调用")
 //    @PreAuthorize("@ss.hasPermission('bpm:process-instance:query')")
@@ -363,9 +430,6 @@ public class BpmProcessInstanceController {
 
         }
 
-        if (5 == pageReqVO.getQueryType()) {
-
-        }
         PageResult<BpmElderlyInfoVO> pageResult = processInstanceService.getMyBpmPage(getLoginUserId(), getLoginUserNickname(), pageReqVO);
         return success(pageResult);
     }
@@ -381,12 +445,61 @@ public class BpmProcessInstanceController {
         return success(pageResult);
     }
 
+    @GetMapping("/check-in-export-excel")
+    @Operation(summary = "导出办理入住流程实例 Excel", description = "内容为办理入住分页的全量数据")
+    @ApiAccessLog(operateType = EXPORT)
+    @TenantIgnore
+    public void exportProcessInstanceCheckInExcel(@Valid CheckInPageReqVO pageReqVO,
+                                                  HttpServletResponse response) throws IOException {
+        pageReqVO.setTenantIds(pageReqVO.getTenantIds() == null ? new Long[]{TenantContextHolder.getTenantId()} : pageReqVO.getTenantIds());
+        pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
+        List<CheckInRecordRespVO> list = processInstanceService.getProcessInstanceCheckInPage(
+                getLoginUserId(), pageReqVO).getList();
+        // 导出 Excel
+        ExcelUtils.write(response, "办理入住.xls", "数据", CheckInRecordExcelVO.class,
+                BeanUtils.toBean(list, CheckInRecordExcelVO.class));
+    }
+
+    @GetMapping("/check-in-wait-page")
+    @Operation(summary = "获取轮候入住流程实例", description = "在【我的流程】菜单中,进行调用")
+    @TenantIgnore
+    public CommonResult<PageResult<CheckInWaitRecordRespVO>> getProcessInstanceCheckInWaitPage(
+            @Valid CheckInWaitPageReqVO pageReqVO) {
+        pageReqVO.setTenantIds(pageReqVO.getTenantIds() == null ? new Long[]{TenantContextHolder.getTenantId()} : pageReqVO.getTenantIds());
+        PageResult<CheckInWaitRecordRespVO> pageResult = processInstanceService.getProcessInstanceCheckInWaitPage(getLoginUserId(), pageReqVO);
+        return success(pageResult);
+    }
+
+    @GetMapping("/check-in-wait-export-excel")
+    @Operation(summary = "导出轮候入住流程实例 Excel", description = "内容为轮候入住分页的全量数据")
+    @ApiAccessLog(operateType = EXPORT)
+    @TenantIgnore
+    public void exportProcessInstanceCheckInWaitExcel(@Valid CheckInWaitPageReqVO pageReqVO,
+                                                      HttpServletResponse response) throws IOException {
+        pageReqVO.setTenantIds(pageReqVO.getTenantIds() == null ? new Long[]{TenantContextHolder.getTenantId()} : pageReqVO.getTenantIds());
+        pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
+        List<CheckInWaitRecordRespVO> list = processInstanceService.getProcessInstanceCheckInWaitPage(
+                getLoginUserId(), pageReqVO).getList();
+        // 导出 Excel
+        ExcelUtils.write(response, "轮候入住.xls", "数据", CheckInWaitRecordExcelVO.class,
+                BeanUtils.toBean(list, CheckInWaitRecordExcelVO.class));
+    }
+
     @GetMapping("/deleteAllHistoricProcessInstance")
     public CommonResult<Boolean> deleteAllHistoricProcessInstance() {
         processInstanceService.deleteAllHistoricProcessInstance();
         return success(Boolean.TRUE);
     }
 
+    @GetMapping("/get-overdue-list")
+    @Operation(summary = "获取超时停留的流程实例列表", description = "根据租户编号与超时天数,获取所有在该节点停留时间超过超时天数的流程信息")
+    @TenantIgnore
+    public CommonResult<List<BpmProcessOverdueRespVO>> getOverdueProcessInstanceList(
+            @Valid BpmProcessOverdueReqVO reqVO) {
+        return success(processInstanceService.getOverdueProcessInstanceList(
+                reqVO.getTenantId(), reqVO.getTimeoutDays()));
+    }
+
     @DeleteMapping("/timedClosureProcess")
     public CommonResult<Boolean> timedClosureProcess(
             @Valid @RequestBody BpmProcessInstanceCancelReqVO cancelReqVO) {

+ 24 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/elderly/CheckInWaitPageReqVO.java

@@ -0,0 +1,24 @@
+package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.elderly;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+@Schema(description = "管理后台 - 轮候入住 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class CheckInWaitPageReqVO extends PageParam {
+
+    @Schema(description = "长者姓名", example = "测试长者")
+    private String elderName;
+
+    @Schema(description = "审批状态,1-审批中,2-审批通过,3-审批不通过,4-已取消,5-系统审批", example = "2")
+    private Integer bpmStatus;
+
+    @Schema(description = "轮候入住业务状态,1-14", example = "1")
+    private Integer status;
+}
+

+ 59 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/excel/BedChangeRecordExcelVO.java

@@ -0,0 +1,59 @@
+package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.excel;
+
+import cn.iocoder.yudao.module.bpm.convert.excel.BpmProcessInstanceStatusConvert;
+import cn.iocoder.yudao.module.bpm.convert.excel.LocalDateConvert;
+import cn.iocoder.yudao.module.bpm.convert.excel.YesNoConvert;
+import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
+import com.alibaba.excel.annotation.ExcelProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDate;
+
+/**
+ * 管理后台 - 床位变更导出 Excel VO
+ *
+ * @author 养老项目
+ */
+@Schema(description = "管理后台 - 床位变更导出 Excel VO")
+@Data
+@ExcelIgnoreUnannotated
+public class BedChangeRecordExcelVO {
+
+    @Schema(description = "长者姓名")
+    @ExcelProperty("长者姓名")
+    private String elderlyName;
+
+    @Schema(description = "原床位")
+    @ExcelProperty("原床位")
+    private String originalName;
+
+    @Schema(description = "原收费项目")
+    @ExcelProperty("原收费项目")
+    private String originalItemName;
+
+    @Schema(description = "变更后床位")
+    @ExcelProperty("变更后床位")
+    private String expectName;
+
+    @Schema(description = "变更后收费项目")
+    @ExcelProperty("变更后收费项目")
+    private String expectItemName;
+
+    @Schema(description = "变更日期")
+    @ExcelProperty(value = "变更日期", converter = LocalDateConvert.class)
+    private LocalDate changeDate;
+
+    @Schema(description = "是否包房")
+    @ExcelProperty(value = "是否包房", converter = YesNoConvert.class)
+    private Integer isPrivateRoom;
+
+    @Schema(description = "审批状态")
+    @ExcelProperty(value = "审批状态", converter = BpmProcessInstanceStatusConvert.class)
+    private Integer status;
+
+    @Schema(description = "所属机构")
+    @ExcelProperty("所属机构")
+    private String tenantName;
+
+}

+ 66 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/excel/CateringChangeRecordExcelVO.java

@@ -0,0 +1,66 @@
+package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.excel;
+
+import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
+import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
+import cn.iocoder.yudao.module.bpm.convert.excel.BpmProcessInstanceStatusConvert;
+import cn.iocoder.yudao.module.bpm.convert.excel.LocalDateConvert;
+import cn.iocoder.yudao.module.system.enums.DictTypeConstants;
+import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
+import com.alibaba.excel.annotation.ExcelProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDate;
+
+/**
+ * 管理后台 - 餐饮变更导出 Excel VO
+ *
+ * @author 养老项目
+ */
+@Schema(description = "管理后台 - 餐饮变更导出 Excel VO")
+@Data
+@ExcelIgnoreUnannotated
+public class CateringChangeRecordExcelVO {
+
+    @Schema(description = "长者姓名")
+    @ExcelProperty("长者姓名")
+    private String elderlyName;
+
+    @Schema(description = "长者性别")
+    @ExcelProperty(value = "长者性别", converter = DictConvert.class)
+    @DictFormat(DictTypeConstants.USER_SEX)
+    private Integer elderSex;
+
+    @Schema(description = "原套餐")
+    @ExcelProperty("原套餐")
+    private String originalName;
+
+    @Schema(description = "变更后套餐")
+    @ExcelProperty("变更后套餐")
+    private String expectName;
+
+    @Schema(description = "餐别")
+    @ExcelProperty("餐别")
+    private String categoryName;
+
+    @Schema(description = "收费项目")
+    @ExcelProperty("收费项目")
+    private String chargeName;
+
+    @Schema(description = "收费项目名称")
+    @ExcelProperty("收费项目名称")
+    private String originalItemName;
+
+    @Schema(description = "变更日期")
+    @ExcelProperty(value = "变更日期", converter = LocalDateConvert.class)
+    private LocalDate changeDate;
+
+    @Schema(description = "审批状态")
+    @ExcelProperty(value = "审批状态", converter = BpmProcessInstanceStatusConvert.class)
+    private Integer status;
+
+    @Schema(description = "所属机构")
+    @ExcelProperty("所属机构")
+    private String tenantName;
+
+}

+ 69 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/excel/CheckInRecordExcelVO.java

@@ -0,0 +1,69 @@
+package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.excel;
+
+import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
+import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
+import cn.iocoder.yudao.module.bpm.convert.excel.BpmProcessInstanceStatusConvert;
+import cn.iocoder.yudao.module.bpm.convert.excel.CheckInStatusConvert;
+import cn.iocoder.yudao.module.bpm.convert.excel.FileStatusConvert;
+import cn.iocoder.yudao.module.bpm.convert.excel.LocalDateTimeConvert;
+import cn.iocoder.yudao.module.system.enums.DictTypeConstants;
+import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
+import com.alibaba.excel.annotation.ExcelProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * 管理后台 - 办理入住导出 Excel VO
+ *
+ * @author 养老项目
+ */
+@Schema(description = "管理后台 - 办理入住导出 Excel VO")
+@Data
+@ExcelIgnoreUnannotated
+public class CheckInRecordExcelVO {
+
+    @Schema(description = "长者姓名")
+    @ExcelProperty("长者姓名")
+    private String elderName;
+
+    @Schema(description = "长者性别")
+    @ExcelProperty(value = "长者性别", converter = DictConvert.class)
+    @DictFormat(DictTypeConstants.USER_SEX)
+    private Integer elderSex;
+
+    @Schema(description = "入住类别")
+    @ExcelProperty(value = "入住类别", converter = DictConvert.class)
+    @DictFormat(DictTypeConstants.IN_STATUS_TYPE)
+    private Integer inStatusType;
+
+    @Schema(description = "护理等级")
+    @ExcelProperty("护理等级")
+    private String nurseLevelName;
+
+    @Schema(description = "入住时间")
+    @ExcelProperty(value = "入住时间", converter = LocalDateTimeConvert.class)
+    private LocalDateTime checkInTime;
+
+    @Schema(description = "文件状态")
+    @ExcelProperty(value = "文件状态", converter = FileStatusConvert.class)
+    private Integer fileStatus;
+
+    @Schema(description = "办理状态")
+    @ExcelProperty(value = "办理状态", converter = CheckInStatusConvert.class)
+    private Integer status;
+
+    @Schema(description = "审批状态")
+    @ExcelProperty(value = "审批状态", converter = BpmProcessInstanceStatusConvert.class)
+    private Integer bpmStatus;
+
+    @Schema(description = "申请时间")
+    @ExcelProperty(value = "申请时间", converter = LocalDateTimeConvert.class)
+    private LocalDateTime createdTime;
+
+    @Schema(description = "所属机构")
+    @ExcelProperty("所属机构")
+    private String tenantName;
+
+}

+ 75 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/excel/CheckInWaitRecordExcelVO.java

@@ -0,0 +1,75 @@
+package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.excel;
+
+import cn.iocoder.yudao.module.bpm.convert.excel.BpmCheckInWaitStatusConvert;
+import cn.iocoder.yudao.module.bpm.convert.excel.BpmProcessInstanceStatusConvert;
+import cn.iocoder.yudao.module.bpm.convert.excel.LocalDateTimeConvert;
+import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
+import com.alibaba.excel.annotation.ExcelProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * 管理后台 - 轮候入住导出 Excel VO
+ *
+ * @author 养老项目
+ */
+@Schema(description = "管理后台 - 轮候入住导出 Excel VO")
+@Data
+@ExcelIgnoreUnannotated
+public class CheckInWaitRecordExcelVO {
+
+    @Schema(description = "长者姓名")
+    @ExcelProperty("长者姓名")
+    private String elderName;
+
+    @Schema(description = "身份证号")
+    @ExcelProperty("身份证号")
+    private String idCard;
+
+    @Schema(description = "最近居住情况")
+    @ExcelProperty("最近居住情况")
+    private String recentLivingSituation;
+
+    @Schema(description = "近六个月病历")
+    @ExcelProperty("近六个月病历")
+    private String medicalRecordsWithinSixMonth;
+
+    @Schema(description = "近期出院小结")
+    @ExcelProperty("近期出院小结")
+    private String recentDischargeSummary;
+
+    @Schema(description = "体检报告")
+    @ExcelProperty("体检报告")
+    private String physicalExaminationReport;
+
+    @Schema(description = "第一审批人")
+    @ExcelProperty("第一审批人")
+    private String firstAuditName;
+
+    @Schema(description = "第二审批人")
+    @ExcelProperty("第二审批人")
+    private String auditName;
+
+    @Schema(description = "备注")
+    @ExcelProperty("备注")
+    private String remark;
+
+    @Schema(description = "审批状态")
+    @ExcelProperty(value = "审批状态", converter = BpmProcessInstanceStatusConvert.class)
+    private Integer bpmStatus;
+
+    @Schema(description = "业务状态")
+    @ExcelProperty(value = "业务状态", converter = BpmCheckInWaitStatusConvert.class)
+    private Integer status;
+
+    @Schema(description = "创建时间")
+    @ExcelProperty(value = "创建时间", converter = LocalDateTimeConvert.class)
+    private LocalDateTime createdTime;
+
+    @Schema(description = "所属机构")
+    @ExcelProperty("所属机构")
+    private String tenantName;
+
+}

+ 59 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/excel/NurseChangeRecordExcelVO.java

@@ -0,0 +1,59 @@
+package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.excel;
+
+import cn.iocoder.yudao.module.bpm.convert.excel.BpmProcessInstanceStatusConvert;
+import cn.iocoder.yudao.module.bpm.convert.excel.LocalDateConvert;
+import cn.iocoder.yudao.module.bpm.convert.excel.YesNoConvert;
+import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
+import com.alibaba.excel.annotation.ExcelProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDate;
+
+/**
+ * 管理后台 - 护理变更导出 Excel VO
+ *
+ * @author 养老项目
+ */
+@Schema(description = "管理后台 - 护理变更导出 Excel VO")
+@Data
+@ExcelIgnoreUnannotated
+public class NurseChangeRecordExcelVO {
+
+    @Schema(description = "长者姓名")
+    @ExcelProperty("长者姓名")
+    private String elderlyName;
+
+    @Schema(description = "原护理等级")
+    @ExcelProperty("原护理等级")
+    private String originalName;
+
+    @Schema(description = "变更后护理等级")
+    @ExcelProperty("变更后护理等级")
+    private String expectName;
+
+    @Schema(description = "收费项目")
+    @ExcelProperty("收费项目")
+    private String chargeName;
+
+    @Schema(description = "收费项目名称")
+    @ExcelProperty("收费项目名称")
+    private String originalItemName;
+
+    @Schema(description = "变更日期")
+    @ExcelProperty(value = "变更日期", converter = LocalDateConvert.class)
+    private LocalDate changeDate;
+
+    @Schema(description = "是否包房")
+    @ExcelProperty(value = "是否包房", converter = YesNoConvert.class)
+    private Integer isPrivateRoom;
+
+    @Schema(description = "审批状态")
+    @ExcelProperty(value = "审批状态", converter = BpmProcessInstanceStatusConvert.class)
+    private Integer status;
+
+    @Schema(description = "所属机构")
+    @ExcelProperty("所属机构")
+    private String tenantName;
+
+}

+ 42 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/excel/PriceChangeRecordExcelVO.java

@@ -0,0 +1,42 @@
+package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.excel;
+
+import cn.iocoder.yudao.module.bpm.convert.excel.BpmProcessInstanceStatusConvert;
+import cn.iocoder.yudao.module.bpm.convert.excel.LocalDateConvert;
+import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
+import com.alibaba.excel.annotation.ExcelProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDate;
+
+/**
+ * 管理后台 - 价格变更导出 Excel VO
+ *
+ * @author 养老项目
+ */
+@Schema(description = "管理后台 - 价格变更导出 Excel VO")
+@Data
+@ExcelIgnoreUnannotated
+public class PriceChangeRecordExcelVO {
+
+    @Schema(description = "长者姓名")
+    @ExcelProperty("长者姓名")
+    private String elderlyName;
+
+    @Schema(description = "收费类别")
+    @ExcelProperty("收费类别")
+    private String categoryName;
+
+    @Schema(description = "变更日期")
+    @ExcelProperty(value = "变更日期", converter = LocalDateConvert.class)
+    private LocalDate changeDate;
+
+    @Schema(description = "审批状态")
+    @ExcelProperty(value = "审批状态", converter = BpmProcessInstanceStatusConvert.class)
+    private Integer status;
+
+    @Schema(description = "所属机构")
+    @ExcelProperty("所属机构")
+    private String tenantName;
+
+}

+ 20 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmProcessOverdueReqVO.java

@@ -0,0 +1,20 @@
+package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+
+@Schema(description = "管理后台 - 超时停留流程实例 Request VO")
+@Data
+public class BpmProcessOverdueReqVO {
+
+    @Schema(description = "租户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    @NotNull(message = "租户编号不能为空")
+    private Long tenantId;
+
+    @Schema(description = "超时天数,流程在某节点停留超过该天数即视为超时", requiredMode = Schema.RequiredMode.REQUIRED, example = "3")
+    @NotNull(message = "超时天数不能为空")
+    private Integer timeoutDays;
+
+}

+ 34 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmProcessOverdueRespVO.java

@@ -0,0 +1,34 @@
+package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "管理后台 - 超时停留流程实例 Response VO")
+@Data
+public class BpmProcessOverdueRespVO {
+
+    @Schema(description = "流程实例编号", example = "bd728f76-5a11-11ef-98ac-00155d6a6c52")
+    private String processInstanceId;
+
+    @Schema(description = "当前停留的节点名称", example = "院长审批")
+    private String taskName;
+
+    @Schema(description = "长者姓名", example = "张三")
+    private String elderName;
+
+    @Schema(description = "流程类型", example = "入住申请")
+    private String processType;
+
+    @Schema(description = "到达该节点的时间", example = "2024-01-01 12:00:00")
+    @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime arriveTime;
+
+    @Schema(description = "审核人", example = "李四")
+    private String reviewer;
+
+}

+ 3 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskPageReqVO.java

@@ -22,6 +22,9 @@ public class BpmTaskPageReqVO extends PageParam {
     @Schema(description = "流程分类", example = "1")
     private String category;
 
+    @Schema(description = "标题", example = "测试标题")
+    private String templateContent;
+
     @Schema(description = "创建时间")
     @DateTimeFormat(pattern = DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
     private LocalDateTime[] createTime;

+ 1 - 1
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/definition/BpmModelConvert.java

@@ -55,7 +55,7 @@ public interface BpmModelConvert {
             if (tenantMap != null && processDefinition != null) {
                 String[] arr = processDefinition.getKey().split("_");
                 if (arr.length > 2) {
-                    Long tenantId = Long.valueOf(arr[2]);
+                    Long tenantId = Long.valueOf(arr[arr.length-1]);
                     tenantInfoRespDTO = tenantMap.get(tenantId);
                 }
             }

+ 50 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/excel/BpmCheckInWaitStatusConvert.java

@@ -0,0 +1,50 @@
+package cn.iocoder.yudao.module.bpm.convert.excel;
+
+import cn.iocoder.yudao.module.bpm.enums.BpmCheckInWaitStatusEnum;
+import com.alibaba.excel.converters.Converter;
+import com.alibaba.excel.enums.CellDataTypeEnum;
+import com.alibaba.excel.metadata.GlobalConfiguration;
+import com.alibaba.excel.metadata.data.ReadCellData;
+import com.alibaba.excel.metadata.data.WriteCellData;
+import com.alibaba.excel.metadata.property.ExcelContentProperty;
+
+/**
+ * Excel 轮候入住业务状态转换器
+ *
+ * @author 养老项目
+ */
+public class BpmCheckInWaitStatusConvert implements Converter<Integer> {
+
+    @Override
+    public Class<?> supportJavaTypeKey() {
+        return Integer.class;
+    }
+
+    @Override
+    public CellDataTypeEnum supportExcelTypeKey() {
+        return CellDataTypeEnum.STRING;
+    }
+
+    @Override
+    public Integer convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty,
+                                     GlobalConfiguration globalConfiguration) {
+        // 导出使用,暂不支持导入
+        return null;
+    }
+
+    @Override
+    public WriteCellData<?> convertToExcelData(Integer value, ExcelContentProperty contentProperty,
+                                               GlobalConfiguration globalConfiguration) {
+        WriteCellData<String> cellData = new WriteCellData<>();
+        String cellValue = "";
+        if (value != null) {
+            BpmCheckInWaitStatusEnum statusEnum = BpmCheckInWaitStatusEnum.valueOfStatus(value);
+            cellValue = statusEnum == null ? String.valueOf(value) : statusEnum.getDesc();
+        }
+        cellData.setType(CellDataTypeEnum.STRING);
+        cellData.setStringValue(cellValue);
+        cellData.setData(cellValue);
+        return cellData;
+    }
+
+}

+ 56 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/excel/BpmProcessInstanceStatusConvert.java

@@ -0,0 +1,56 @@
+package cn.iocoder.yudao.module.bpm.convert.excel;
+
+import cn.hutool.core.util.ArrayUtil;
+import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceStatusEnum;
+import com.alibaba.excel.converters.Converter;
+import com.alibaba.excel.enums.CellDataTypeEnum;
+import com.alibaba.excel.metadata.GlobalConfiguration;
+import com.alibaba.excel.metadata.data.ReadCellData;
+import com.alibaba.excel.metadata.data.WriteCellData;
+import com.alibaba.excel.metadata.property.ExcelContentProperty;
+
+/**
+ * Excel 审批状态转换器:1-审批中,2-审批通过,3-审批不通过,4-已取消,5-系统审批
+ *
+ * @author 养老项目
+ */
+public class BpmProcessInstanceStatusConvert implements Converter<Integer> {
+
+    @Override
+    public Class<?> supportJavaTypeKey() {
+        return Integer.class;
+    }
+
+    @Override
+    public CellDataTypeEnum supportExcelTypeKey() {
+        return CellDataTypeEnum.STRING;
+    }
+
+    @Override
+    public Integer convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty,
+                                     GlobalConfiguration globalConfiguration) {
+        // 导出使用,暂不支持导入
+        return null;
+    }
+
+    @Override
+    public WriteCellData<?> convertToExcelData(Integer value, ExcelContentProperty contentProperty,
+                                               GlobalConfiguration globalConfiguration) {
+        WriteCellData<String> cellData = new WriteCellData<>();
+        String cellValue = "";
+        if (value != null) {
+            if (value == 5) {
+                cellValue = "系统审批";
+            } else {
+                BpmProcessInstanceStatusEnum statusEnum = ArrayUtil.firstMatch(
+                        item -> item.getStatus().equals(value), BpmProcessInstanceStatusEnum.values());
+                cellValue = statusEnum == null ? String.valueOf(value) : statusEnum.getDesc();
+            }
+        }
+        cellData.setType(CellDataTypeEnum.STRING);
+        cellData.setStringValue(cellValue);
+        cellData.setData(cellValue);
+        return cellData;
+    }
+
+}

+ 60 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/excel/CheckInStatusConvert.java

@@ -0,0 +1,60 @@
+package cn.iocoder.yudao.module.bpm.convert.excel;
+
+import com.alibaba.excel.converters.Converter;
+import com.alibaba.excel.enums.CellDataTypeEnum;
+import com.alibaba.excel.metadata.GlobalConfiguration;
+import com.alibaba.excel.metadata.data.ReadCellData;
+import com.alibaba.excel.metadata.data.WriteCellData;
+import com.alibaba.excel.metadata.property.ExcelContentProperty;
+
+/**
+ * Excel 办理入住状态转换器:1-待入住办理,2-已入住,3-退住
+ *
+ * @author 养老项目
+ */
+public class CheckInStatusConvert implements Converter<Integer> {
+
+    @Override
+    public Class<?> supportJavaTypeKey() {
+        return Integer.class;
+    }
+
+    @Override
+    public CellDataTypeEnum supportExcelTypeKey() {
+        return CellDataTypeEnum.STRING;
+    }
+
+    @Override
+    public Integer convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty,
+                                     GlobalConfiguration globalConfiguration) {
+        // 导出使用,暂不支持导入
+        return null;
+    }
+
+    @Override
+    public WriteCellData<?> convertToExcelData(Integer value, ExcelContentProperty contentProperty,
+                                               GlobalConfiguration globalConfiguration) {
+        WriteCellData<String> cellData = new WriteCellData<>();
+        String cellValue = "";
+        if (value != null) {
+            switch (value) {
+                case 1:
+                    cellValue = "待入住办理";
+                    break;
+                case 2:
+                    cellValue = "已入住";
+                    break;
+                case 3:
+                    cellValue = "退住";
+                    break;
+                default:
+                    cellValue = String.valueOf(value);
+            }
+        }
+        cellData.setType(CellDataTypeEnum.STRING);
+        cellData.setStringValue(cellValue);
+        cellData.setData(cellValue);
+        return cellData;
+    }
+
+}

+ 57 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/excel/FileStatusConvert.java

@@ -0,0 +1,57 @@
+package cn.iocoder.yudao.module.bpm.convert.excel;
+
+import com.alibaba.excel.converters.Converter;
+import com.alibaba.excel.enums.CellDataTypeEnum;
+import com.alibaba.excel.metadata.GlobalConfiguration;
+import com.alibaba.excel.metadata.data.ReadCellData;
+import com.alibaba.excel.metadata.data.WriteCellData;
+import com.alibaba.excel.metadata.property.ExcelContentProperty;
+
+/**
+ * Excel 文件状态转换器:0-未上传,1-已上传
+ *
+ * @author 养老项目
+ */
+public class FileStatusConvert implements Converter<Integer> {
+
+    @Override
+    public Class<?> supportJavaTypeKey() {
+        return Integer.class;
+    }
+
+    @Override
+    public CellDataTypeEnum supportExcelTypeKey() {
+        return CellDataTypeEnum.STRING;
+    }
+
+    @Override
+    public Integer convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty,
+                                     GlobalConfiguration globalConfiguration) {
+        // 导出使用,暂不支持导入
+        return null;
+    }
+
+    @Override
+    public WriteCellData<?> convertToExcelData(Integer value, ExcelContentProperty contentProperty,
+                                               GlobalConfiguration globalConfiguration) {
+        WriteCellData<String> cellData = new WriteCellData<>();
+        String cellValue = "";
+        if (value != null) {
+            switch (value) {
+                case 0:
+                    cellValue = "未上传";
+                    break;
+                case 1:
+                    cellValue = "已上传";
+                    break;
+                default:
+                    cellValue = String.valueOf(value);
+            }
+        }
+        cellData.setType(CellDataTypeEnum.STRING);
+        cellData.setStringValue(cellValue);
+        cellData.setData(cellValue);
+        return cellData;
+    }
+
+}

+ 49 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/excel/LocalDateConvert.java

@@ -0,0 +1,49 @@
+package cn.iocoder.yudao.module.bpm.convert.excel;
+
+import com.alibaba.excel.converters.Converter;
+import com.alibaba.excel.enums.CellDataTypeEnum;
+import com.alibaba.excel.metadata.GlobalConfiguration;
+import com.alibaba.excel.metadata.data.ReadCellData;
+import com.alibaba.excel.metadata.data.WriteCellData;
+import com.alibaba.excel.metadata.property.ExcelContentProperty;
+
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+
+/**
+ * Excel {@link LocalDate} 转换器,格式化为 yyyy-MM-dd
+ *
+ * @author 养老项目
+ */
+public class LocalDateConvert implements Converter<LocalDate> {
+
+    private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+
+    @Override
+    public Class<?> supportJavaTypeKey() {
+        return LocalDate.class;
+    }
+
+    @Override
+    public CellDataTypeEnum supportExcelTypeKey() {
+        return CellDataTypeEnum.STRING;
+    }
+
+    @Override
+    public LocalDate convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty,
+                                       GlobalConfiguration globalConfiguration) {
+        return LocalDate.parse(cellData.getStringValue(), FORMATTER);
+    }
+
+    @Override
+    public WriteCellData<?> convertToExcelData(LocalDate value, ExcelContentProperty contentProperty,
+                                               GlobalConfiguration globalConfiguration) {
+        WriteCellData<String> cellData = new WriteCellData<>();
+        String cellValue = value == null ? "" : FORMATTER.format(value);
+        cellData.setType(CellDataTypeEnum.STRING);
+        cellData.setStringValue(cellValue);
+        cellData.setData(cellValue);
+        return cellData;
+    }
+
+}

+ 49 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/excel/LocalDateTimeConvert.java

@@ -0,0 +1,49 @@
+package cn.iocoder.yudao.module.bpm.convert.excel;
+
+import com.alibaba.excel.converters.Converter;
+import com.alibaba.excel.enums.CellDataTypeEnum;
+import com.alibaba.excel.metadata.GlobalConfiguration;
+import com.alibaba.excel.metadata.data.ReadCellData;
+import com.alibaba.excel.metadata.data.WriteCellData;
+import com.alibaba.excel.metadata.property.ExcelContentProperty;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+
+/**
+ * Excel {@link LocalDateTime} 转换器,格式化为 yyyy-MM-dd HH:mm:ss
+ *
+ * @author 养老项目
+ */
+public class LocalDateTimeConvert implements Converter<LocalDateTime> {
+
+    private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+
+    @Override
+    public Class<?> supportJavaTypeKey() {
+        return LocalDateTime.class;
+    }
+
+    @Override
+    public CellDataTypeEnum supportExcelTypeKey() {
+        return CellDataTypeEnum.STRING;
+    }
+
+    @Override
+    public LocalDateTime convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty,
+                                           GlobalConfiguration globalConfiguration) {
+        return LocalDateTime.parse(cellData.getStringValue(), FORMATTER);
+    }
+
+    @Override
+    public WriteCellData<?> convertToExcelData(LocalDateTime value, ExcelContentProperty contentProperty,
+                                               GlobalConfiguration globalConfiguration) {
+        WriteCellData<String> cellData = new WriteCellData<>();
+        String cellValue = value == null ? "" : FORMATTER.format(value);
+        cellData.setType(CellDataTypeEnum.STRING);
+        cellData.setStringValue(cellValue);
+        cellData.setData(cellValue);
+        return cellData;
+    }
+
+}

+ 45 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/excel/YesNoConvert.java

@@ -0,0 +1,45 @@
+package cn.iocoder.yudao.module.bpm.convert.excel;
+
+import com.alibaba.excel.converters.Converter;
+import com.alibaba.excel.enums.CellDataTypeEnum;
+import com.alibaba.excel.metadata.GlobalConfiguration;
+import com.alibaba.excel.metadata.data.ReadCellData;
+import com.alibaba.excel.metadata.data.WriteCellData;
+import com.alibaba.excel.metadata.property.ExcelContentProperty;
+
+/**
+ * Excel 是否转换器:1-是,其它-否
+ *
+ * @author 养老项目
+ */
+public class YesNoConvert implements Converter<Integer> {
+
+    @Override
+    public Class<?> supportJavaTypeKey() {
+        return Integer.class;
+    }
+
+    @Override
+    public CellDataTypeEnum supportExcelTypeKey() {
+        return CellDataTypeEnum.STRING;
+    }
+
+    @Override
+    public Integer convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty,
+                                     GlobalConfiguration globalConfiguration) {
+        // 导出使用,暂不支持导入
+        return null;
+    }
+
+    @Override
+    public WriteCellData<?> convertToExcelData(Integer value, ExcelContentProperty contentProperty,
+                                               GlobalConfiguration globalConfiguration) {
+        WriteCellData<String> cellData = new WriteCellData<>();
+        String cellValue = value == null ? "" : (value == 1 ? "是" : "否");
+        cellData.setType(CellDataTypeEnum.STRING);
+        cellData.setStringValue(cellValue);
+        cellData.setData(cellValue);
+        return cellData;
+    }
+
+}

+ 4 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/definition/BpmFormDO.java

@@ -56,5 +56,9 @@ public class BpmFormDO extends BaseDO {
      * 备注
      */
     private String remark;
+    /**
+     * 机构id
+     */
+    private Long tenantId;
 
 }

+ 21 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/definition/BpmTaskCopyRelationDO.java

@@ -0,0 +1,21 @@
+package cn.iocoder.yudao.module.bpm.dal.dataobject.definition;
+
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@TableName(value = "bpm_task_copy_relation")
+@KeySequence("bpm_task_copy_relation_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class BpmTaskCopyRelationDO {
+    private Long id;
+    private String taskDefinitionKey;
+    private String userIdList;
+    private Long tenantId;
+}

+ 1 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/mysql/definition/BpmFormMapper.java

@@ -18,6 +18,7 @@ public interface BpmFormMapper extends BaseMapperX<BpmFormDO> {
 
     default PageResult<BpmFormDO> selectPage(BpmFormPageReqVO reqVO) {
         return selectPage(reqVO, new QueryWrapperX<BpmFormDO>()
+                .inIfPresent("tenant_id",reqVO.getTenantIds())
                 .likeIfPresent("name", reqVO.getName())
                 .orderByDesc("id"));
     }

+ 9 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/mysql/definition/BpmTaskCopyRelationMapper.java

@@ -0,0 +1,9 @@
+package cn.iocoder.yudao.module.bpm.dal.mysql.definition;
+
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmTaskCopyRelationDO;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface BpmTaskCopyRelationMapper extends BaseMapperX<BpmTaskCopyRelationDO> {
+}

+ 2 - 1
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/enums/BpmBusinessTypeEnum.java

@@ -24,7 +24,8 @@ public enum BpmBusinessTypeEnum implements ArrayValuable<Integer> {
     CATERING_CHANGE(6, "catering_change"), //餐饮变更
     PRICE_CHANGE(7, "price_change"), //价格变更
     TRANSFER_CHANGE(8, "transfer_change"), //转院变更
-    CHECK_IN(9, "check_in"); //办理入住
+    CHECK_IN(9, "check_in"), //办理入住
+    CHECK_IN_WAIT(10, "check_in_wait"); //轮候入住
 
     public static final Integer[] ARRAYS = Arrays.stream(values()).map(BpmBusinessTypeEnum::getValue).toArray(Integer[]::new);
 

+ 46 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/enums/BpmCheckInWaitStatusEnum.java

@@ -0,0 +1,46 @@
+package cn.iocoder.yudao.module.bpm.enums;
+
+import cn.hutool.core.util.ArrayUtil;
+import cn.iocoder.yudao.framework.common.core.ArrayValuable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+
+/**
+ * 轮候入住业务状态
+ */
+@AllArgsConstructor
+@Getter
+public enum BpmCheckInWaitStatusEnum implements ArrayValuable<Integer> {
+
+    SUBMITTED_WAIT_FIRST_APPROVAL(1, "已提交,待第一审批人"),
+    FIRST_APPROVAL_PASSED_WAIT_SECOND_APPROVAL(2, "第一审批通过,待第二审批人"),
+    SECOND_APPROVAL_PASSED_WAIT_THIRD_APPROVAL(3, "第二审批通过,待第三审批人"),
+    THIRD_APPROVAL_REJECT_TO_SECOND(4, "第三审批退回第二审批人"),
+    THIRD_APPROVAL_PASSED_WAIT_FOURTH_APPROVAL(5, "第三审批通过,待第四审批人"),
+    FOURTH_APPROVAL_REJECT_TO_THIRD(6, "第四审批退回第三审批人"),
+    FOURTH_APPROVAL_PASSED_WAIT_FIFTH_APPROVAL(7, "第四审批通过,待第五审批人"),
+    FIFTH_APPROVAL_PASSED_WAIT_SIXTH_APPROVAL(8, "第五审批通过,待第六审批人"),
+    SIXTH_APPROVAL_PASSED_WAIT_BED_FILL(9, "第六审批通过待补床位"),
+    APPROVED_FINISHED(10, "流程审批完成"),
+    FIRST_APPROVAL_REJECT_TO_FAMILY(11, "第一审批驳回退家属"),
+    SECOND_APPROVAL_REJECT_FINISHED(12, "第二审批驳回结束"),
+    FIFTH_APPROVAL_REJECT_FINISHED(13, "第五审批驳回结束"),
+    SIXTH_APPROVAL_REJECT_TO_FIFTH(14, "第六审批驳回退第五审批人");
+
+    public static final Integer[] ARRAYS = Arrays.stream(values()).map(BpmCheckInWaitStatusEnum::getStatus).toArray(Integer[]::new);
+
+    private final Integer status;
+
+    private final String desc;
+
+    public static BpmCheckInWaitStatusEnum valueOfStatus(Integer status) {
+        return ArrayUtil.firstMatch(item -> item.getStatus().equals(status), BpmCheckInWaitStatusEnum.values());
+    }
+
+    @Override
+    public Integer[] array() {
+        return ARRAYS;
+    }
+}

+ 3 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmProcessInstanceEventListener.java

@@ -42,6 +42,9 @@ public class BpmProcessInstanceEventListener extends AbstractFlowableEngineEvent
     @Override // 特殊情况:当跳转到 EndEvent 流程实例未结束, 会执行 deleteProcessInstance 方法
     protected void processCancelled(FlowableCancelledEvent event) {
         ProcessInstance processInstance = processInstanceService.getProcessInstance(event.getProcessInstanceId());
+        if (processInstance == null) {
+            return;
+        }
         processInstanceService.processProcessInstanceCompleted(processInstance);
     }
 

+ 12 - 7
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/business/BpmBusinessFormServiceImpl.java

@@ -5,6 +5,7 @@ import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
 import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
 import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
+import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
 import cn.iocoder.yudao.module.bpm.api.task.BpmProcessInstanceApi;
 import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO;
 import cn.iocoder.yudao.module.bpm.constant.BpmConstant;
@@ -81,7 +82,13 @@ public class BpmBusinessFormServiceImpl implements BpmBusinessFormService {
     @Override
     @Transactional(rollbackFor = Exception.class)
     public Long create(BpmBusinessFormCreateReqVO createReqVO) {
+        if (createReqVO.getTenantId() == null) {
+            return create0(createReqVO);
+        }
+        return TenantUtils.execute(createReqVO.getTenantId(), () -> create0(createReqVO));
+    }
 
+    private Long create0(BpmBusinessFormCreateReqVO createReqVO) {
         // 1. 插入表单
 
         Integer status = modelConfigService.getModelStatus(String.format(BpmConstant.BPM_PROCESS_KEY_FORMAT,
@@ -89,7 +96,7 @@ public class BpmBusinessFormServiceImpl implements BpmBusinessFormService {
                 createReqVO.getTenantId()));
         // 插入业务表单中间表
         BpmBusinessFormDO businessFormDO = BeanUtils.toBean(createReqVO, BpmBusinessFormDO.class);
-        Long userId = getLoginUserId();
+        Long userId = createReqVO.getUserId() != null ? createReqVO.getUserId() : getLoginUserId();
         LocalDate nowLocalDate = LocalDate.now();
         if (status == 1) {
             // 修改为系统审批状态
@@ -206,12 +213,10 @@ public class BpmBusinessFormServiceImpl implements BpmBusinessFormService {
 
     @Override
     public BpmBusinessFormDO getBusinessFormByBusinessId(String businessId) {
-        BpmBusinessFormDO businessFormDO = new BpmBusinessFormDO();
-        List<BpmBusinessFormDO> bpmBusinessFormDOS = bpmBusinessFormMapper.selectList(new LambdaQueryWrapperX<BpmBusinessFormDO>().eq(BpmBusinessFormDO::getBusinessId, businessId));
-        if (null != bpmBusinessFormDOS) {
-            businessFormDO = bpmBusinessFormDOS.get(0);
-        }
-        return businessFormDO;
+        return bpmBusinessFormMapper.selectOne(new LambdaQueryWrapperX<BpmBusinessFormDO>()
+                .eq(BpmBusinessFormDO::getBusinessId, businessId)
+                .orderByDesc(BpmBusinessFormDO::getId)
+                .last("LIMIT 1"));
     }
 
     @Override

+ 18 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/business/BpmElderlApplyService.java

@@ -3,6 +3,8 @@ package cn.iocoder.yudao.module.bpm.service.business;
 import cn.iocoder.yudao.module.bpm.controller.admin.business.vo.*;
 import cn.iocoder.yudao.module.system.api.bpm.vo.CheckInCreateReqVO;
 
+import java.util.Map;
+
 /**
  * @Author: jinyd
  * @CreateTime: 2024/8/6  10:55
@@ -23,17 +25,33 @@ public interface BpmElderlApplyService {
 
     Long createApplyRetreat(Long userId, BpmApplyRetreatCreateReqVO createReqVO);
 
+    /**
+     * 退住申请(离住转退住)
+     * <p>
+     * 不需要校验是否存在未返院的离住记录,也不需要校验费用截止日期月有无该长者账单。
+     *
+     * @param userId      用户编号
+     * @param createReqVO 创建请求参数
+     * @return 退住申请编号
+     */
+    Long createApplyRetreatSimple(Long userId, BpmApplyRetreatSimpleCreateReqVO createReqVO);
+
+    void revokeApprovedRetreat(Long userId, Long retreatRecordId);
+
     Long bedChangeCreate(Long loginUserId, BedChangeRecordCreateReqVO createReqVO);
 
     Long nurseChangeCreate(Long loginUserId, NurseChangeRecordCreateReqVO createReqVO);
 
     Long cateringChangeCreate(Long loginUserId, CateringChangeRecordCreateReqVO createReqVO);
 
+    Map<String, Object> cateringChangeBatchCreate(Long loginUserId, CateringChangeBatchCreateReqVO reqVO);
+
     Long priceChangeCreate(Long loginUserId, PriceChangeRecordCreateReqVO createReqVO);
 
     Long transferChangeCreate(Long loginUserId, TransferChangeRecordCreateReqVO createReqVO);
 
     Long checkInCreate(Long loginUserId, CheckInCreateReqVO createReqVO);
 
+    Long checkInWaitCreate(Long loginUserId, String nickname, BpmCheckInWaitCreateReqVO createReqVO);
 
 }

+ 220 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/business/BpmElderlApplyServiceImpl.java

@@ -6,11 +6,14 @@ import cn.iocoder.yudao.framework.common.util.string.StringUtil;
 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
 import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
 import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
+import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
 import cn.iocoder.yudao.module.bpm.controller.admin.business.vo.*;
 import cn.iocoder.yudao.module.bpm.dal.dataobject.business.BpmBusinessFormDO;
 import cn.iocoder.yudao.module.bpm.dal.mysql.business.BpmBusinessFormMapper;
 import cn.iocoder.yudao.module.bpm.enums.BpmBusinessTypeEnum;
+import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum;
 import cn.iocoder.yudao.module.system.api.bpm.BpmCheckInApi;
+import cn.iocoder.yudao.module.system.api.bpm.BpmCheckInWaitApi;
 import cn.iocoder.yudao.module.system.api.bpm.BpmElderlyExpenseApi;
 import cn.iocoder.yudao.module.system.api.bpm.vo.*;
 import cn.iocoder.yudao.module.system.api.notify.NotifyMessageSendApi;
@@ -20,13 +23,20 @@ import cn.iocoder.yudao.module.system.api.bpm.BpmElderlyChangeApi;
 import com.mzt.logapi.context.LogRecordContext;
 import com.mzt.logapi.starter.annotation.LogRecord;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Lazy;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.validation.annotation.Validated;
 
 import javax.annotation.Resource;
 
+import java.math.BigDecimal;
 import java.time.LocalDate;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
@@ -59,6 +69,9 @@ public class BpmElderlApplyServiceImpl implements BpmElderlApplyService {
     @Resource
     private BpmCheckInApi checkInApi;
 
+    @Resource
+    private BpmCheckInWaitApi checkInWaitApi;
+
     @Resource
     private BpmBusinessFormMapper bpmBusinessFormMapper;
 
@@ -68,6 +81,10 @@ public class BpmElderlApplyServiceImpl implements BpmElderlApplyService {
     @Resource
     private BpmElderlyExpenseApi elderlyExpenseApi;
 
+    @Resource
+    @Lazy
+    private BpmElderlApplyService self;
+
     @Override
     @Transactional(rollbackFor = Exception.class)
     @LogRecord(type = DMISSION_DISCHARGE_TYPE, subType = DMISSION_ELDER_APPLY_SUB_TYPE, bizNo = "{{#elderId}}",
@@ -161,6 +178,67 @@ public class BpmElderlApplyServiceImpl implements BpmElderlApplyService {
         return bpmBusinessFormService.create(businessFormCreateReqVO);
     }
 
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    @LogRecord(type = DMISSION_RETREAT_TYPE, subType = DMISSION_ELDER_RETREAT_SUB_TYPE, bizNo = "{{#createReqVO.elderlyId}}",
+            success = DMISSION_ELDER_RETREAT_SUCCESS, extra = "{{#createReqVO.elderlyId}}")
+    public Long createApplyRetreatSimple(Long userId, BpmApplyRetreatSimpleCreateReqVO createReqVO) {
+        // 离住转退住:若费用截止日期当月无月度账单则生成;并对离住记录做返院登记(返院日期为费用截止日期)
+        elderlyExpenseApi.handleTempOutReturnForRetreat(createReqVO.getTempOutId(), createReqVO.getElderlyId(),
+                TenantContextHolder.getTenantId(), createReqVO.getCostDeadline());
+        elderlyExpenseApi.applyRetreatCommonCheck(createReqVO.getElderlyId(), createReqVO.getCostDeadline());
+        ElderlyRetreatRecordRespVO elderlyRetreatRecordRespVO = new ElderlyRetreatRecordRespVO();
+        elderlyRetreatRecordRespVO.setElderlyId(createReqVO.getElderlyId());
+        elderlyRetreatRecordRespVO.setRetreatDate(createReqVO.getRetreatDate());
+        elderlyRetreatRecordRespVO.setCostDeadlineDate(createReqVO.getCostDeadline());
+        elderlyRetreatRecordRespVO.setReason(createReqVO.getRetreatReason());
+        elderlyRetreatRecordRespVO.setDescription(createReqVO.getRetreatDescribe());
+        elderlyRetreatRecordRespVO.setApplyType(1);
+        // 生成退住申请记录
+        Long id = bpmElderlyApi.addElderlyRetreatRecord(elderlyRetreatRecordRespVO);
+
+        BpmBusinessFormCreateReqVO businessFormCreateReqVO = new BpmBusinessFormCreateReqVO();
+        businessFormCreateReqVO.setBusinessId(id);
+        businessFormCreateReqVO.setBusinessId2(elderlyRetreatRecordRespVO.getElderlyId());
+        businessFormCreateReqVO.setUserId(userId);
+        businessFormCreateReqVO.setType(BpmBusinessTypeEnum.APPLY_RETREAT.getValue());
+        businessFormCreateReqVO.setTenantId(TenantContextHolder.getTenantId());
+        ElderlyInfoReqVO elderlyInfo = bpmElderlyApi.getElderlyInfo(elderlyRetreatRecordRespVO.getElderlyId());
+        if (null != elderlyInfo) {
+            LogRecordContext.putVariable("id", elderlyInfo.getId());
+            LogRecordContext.putVariable("elderName", elderlyInfo.getElderName());
+        }
+        LogRecordContext.putVariable("nickname", SecurityFrameworkUtils.getLoginUserNickname());
+        return bpmBusinessFormService.create(businessFormCreateReqVO);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    @TenantIgnore
+    public void revokeApprovedRetreat(Long userId, Long retreatRecordId) {
+        BpmBusinessFormDO businessFormDO = bpmBusinessFormMapper.selectOne(new LambdaQueryWrapperX<BpmBusinessFormDO>()
+                .eq(BpmBusinessFormDO::getBusinessId, retreatRecordId)
+                .eq(BpmBusinessFormDO::getType, BpmBusinessTypeEnum.APPLY_RETREAT.getValue())
+                .orderByDesc(BpmBusinessFormDO::getId)
+                .last("LIMIT 1"));
+        if (businessFormDO == null) {
+            throw new RuntimeException("找不到对应的退住申请流程记录");
+        }
+        if (!Objects.equals(businessFormDO.getStatus(), BpmTaskStatusEnum.APPROVE.getStatus())
+                && !Objects.equals(businessFormDO.getStatus(), 5)) {
+            throw new RuntimeException("该退住申请未处于审批通过状态,无法撤销");
+        }
+
+        elderlyExpenseApi.revokeApprovedRetreat(retreatRecordId);
+        bpmElderlyApi.revokeApprovedRetreat(retreatRecordId);
+
+        BpmBusinessFormDO update = new BpmBusinessFormDO();
+        update.setId(businessFormDO.getId());
+        update.setStatus(BpmTaskStatusEnum.CANCEL.getStatus());
+        update.setCompleteStatus(1);
+        bpmBusinessFormMapper.updateById(update);
+    }
+
     @Override
     @Transactional(rollbackFor = Exception.class)
     @LogRecord(type = ELDER_BED_CHANGE_TYPE, subType = ELDER_BED_CHANGE_SUB_TYPE, bizNo = "{{#createReqVO.elderlyId}}",
@@ -250,6 +328,133 @@ public class BpmElderlApplyServiceImpl implements BpmElderlApplyService {
         return bpmBusinessFormService.create(businessFormCreateReqVO);
     }
 
+    @Override
+    @TenantIgnore
+    public Map<String, Object> cateringChangeBatchCreate(Long loginUserId, CateringChangeBatchCreateReqVO reqVO) {
+        Integer identification = 3;
+        List<Long> originalOverheadChargeIds = new ArrayList<>();
+        for (CateringChangeBatchCreateReqVO.ItemMapping mapping : reqVO.getMappings()) {
+            if (mapping.getOriginalOverheadChargeId() != null) {
+                originalOverheadChargeIds.add(mapping.getOriginalOverheadChargeId());
+            }
+        }
+
+        List<ElderlyCurrentChargeItemRespVO> currentItems = elderlyChangeApi.listCurrentChargeItemList(
+                reqVO.getTenantId(), reqVO.getOrgType(), identification, originalOverheadChargeIds.isEmpty() ? null : originalOverheadChargeIds);
+
+        Map<Long, CateringChangeBatchCreateReqVO.ItemMapping> mappingByOriginalId = new LinkedHashMap<>();
+        List<CateringChangeBatchCreateReqVO.ItemMapping> fuzzyMappings = new ArrayList<>();
+        for (CateringChangeBatchCreateReqVO.ItemMapping mapping : reqVO.getMappings()) {
+            if (mapping.getOriginalOverheadChargeId() != null) {
+                mappingByOriginalId.put(mapping.getOriginalOverheadChargeId(), mapping);
+            } else {
+                fuzzyMappings.add(mapping);
+            }
+        }
+
+        List<Long> successProcessIds = new ArrayList<>();
+        List<Long> skippedElderlyIds = new ArrayList<>();
+        Map<Long, String> failedElderlyIds = new LinkedHashMap<>();
+        int matchedCount = 0;
+
+        for (ElderlyCurrentChargeItemRespVO current : currentItems) {
+            CateringChangeBatchCreateReqVO.ItemMapping mapping = mappingByOriginalId.get(current.getOverheadChargeId());
+            if (mapping == null) {
+                for (CateringChangeBatchCreateReqVO.ItemMapping candidate : fuzzyMappings) {
+                    if (candidate.getOriginalName() == null || candidate.getOriginalActualAmount() == null) {
+                        continue;
+                    }
+                    if (!candidate.getOriginalName().equals(current.getItemName())) {
+                        continue;
+                    }
+                    if (current.getActualAmount() == null) {
+                        continue;
+                    }
+                    if (candidate.getOriginalActualAmount().compareTo(current.getActualAmount()) != 0) {
+                        continue;
+                    }
+                    mapping = candidate;
+                    break;
+                }
+            }
+            if (mapping == null) {
+                continue;
+            }
+            matchedCount++;
+
+            if (Boolean.TRUE.equals(reqVO.getSkipRepeat())) {
+                Boolean repeated = elderlyChangeApi.checkRepeat(current.getElderlyId(), reqVO.getChangeDate(),
+                        ChangeTypeEnum.CATERING.getValue(), reqVO.getTenantId());
+                if (Boolean.TRUE.equals(repeated)) {
+                    skippedElderlyIds.add(current.getElderlyId());
+                    continue;
+                }
+            }
+
+            CateringChangeRecordCreateReqVO createReqVO = new CateringChangeRecordCreateReqVO();
+            createReqVO.setElderlyId(current.getElderlyId());
+            createReqVO.setOriginalId(current.getOverheadChargeId());
+            createReqVO.setOriginalName(current.getItemName());
+            createReqVO.setChangeDate(reqVO.getChangeDate());
+            createReqVO.setExpectId(mapping.getExpectOverheadChargeId());
+            createReqVO.setExpectName(mapping.getExpectName());
+            createReqVO.setCategoryName(reqVO.getCategoryName());
+            createReqVO.setOverheadChargeId(current.getOverheadChargeId());
+            createReqVO.setExpectOverheadChargeId(mapping.getExpectOverheadChargeId());
+            createReqVO.setAmount(mapping.getAmount());
+
+            BigDecimal discountAmount = null;
+            Integer isDiscount = 0;
+            String discount = null;
+            if (Boolean.TRUE.equals(reqVO.getKeepDiscount())) {
+                discountAmount = current.getDiscountAmount();
+                isDiscount = current.getIsDiscount() == null ? 0 : current.getIsDiscount();
+                discount = current.getDiscount();
+            }
+            createReqVO.setIsDiscount(isDiscount);
+            createReqVO.setDiscountAmount(discountAmount);
+            createReqVO.setDiscount(discount);
+
+            BigDecimal actualAmount = mapping.getActualAmount();
+            if (actualAmount == null) {
+                if (Boolean.TRUE.equals(reqVO.getKeepDiscount()) && discountAmount != null) {
+                    actualAmount = mapping.getAmount().subtract(discountAmount);
+                } else {
+                    actualAmount = mapping.getAmount();
+                }
+            }
+            createReqVO.setActualAmount(actualAmount);
+
+            createReqVO.setChangeFiles(StringUtil.isEmpty(reqVO.getChangeFiles()) ? "[]" : reqVO.getChangeFiles());
+            createReqVO.setReason(reqVO.getReason());
+            createReqVO.setType(BpmBusinessTypeEnum.CATERING_CHANGE.getValue());
+            createReqVO.setTenantId(reqVO.getTenantId());
+            createReqVO.setOriginalAmount(current.getActualAmount());
+            createReqVO.setHysteresisMonth(reqVO.getHysteresisMonth());
+
+            try {
+                Long processId = self.cateringChangeCreate(loginUserId, createReqVO);
+                successProcessIds.add(processId);
+            } catch (Exception e) {
+                failedElderlyIds.put(current.getElderlyId(), Objects.toString(e.getMessage(), "ERROR"));
+            }
+        }
+
+        Map<String, Object> result = new LinkedHashMap<>();
+        result.put("tenantId", reqVO.getTenantId());
+        result.put("orgType", reqVO.getOrgType());
+        result.put("changeDate", reqVO.getChangeDate());
+        result.put("matchedCount", matchedCount);
+        result.put("successCount", successProcessIds.size());
+        result.put("successProcessIds", successProcessIds);
+        result.put("skippedCount", skippedElderlyIds.size());
+        result.put("skippedElderlyIds", skippedElderlyIds);
+        result.put("failedCount", failedElderlyIds.size());
+        result.put("failedElderlyIds", failedElderlyIds);
+        result.put("totalCurrentCateringCount", currentItems.size());
+        return result;
+    }
+
     @Override
     @Transactional(rollbackFor = Exception.class)
     @LogRecord(type = ELDER_PRICE_CHANGE_TYPE, subType = ELDER_PRICE_CHANGE_SUB_TYPE, bizNo = "{{#createReqVO.elderlyId}}",
@@ -349,6 +554,8 @@ public class BpmElderlApplyServiceImpl implements BpmElderlApplyService {
         if (null != elderlyInfo) {
             LogRecordContext.putVariable("id", elderlyInfo.getId());
             LogRecordContext.putVariable("elderName", elderlyInfo.getElderName());
+            elderlyInfo.setBedId(createReqVO.getExpenseBO().getBedId());
+            bpmElderlyApi.update(elderlyInfo);
         }
         LogRecordContext.putVariable("nickname", SecurityFrameworkUtils.getLoginUserNickname());
         createReqVO.setBusinessId2(createReqVO.getElderlyId());
@@ -357,4 +564,17 @@ public class BpmElderlApplyServiceImpl implements BpmElderlApplyService {
         BpmBusinessFormCreateReqVO businessFormCreateReqVO = BeanUtils.toBean(createReqVO, BpmBusinessFormCreateReqVO.class);
         return bpmBusinessFormService.create(businessFormCreateReqVO);
     }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Long checkInWaitCreate(Long loginUserId, String nickname, BpmCheckInWaitCreateReqVO createReqVO) {
+        CheckInWaitCreateReqVO reqVO = BeanUtils.toBean(createReqVO, CheckInWaitCreateReqVO.class);
+        reqVO.setStatus(1);
+        Long id = checkInWaitApi.createCheckInWait(reqVO);
+        createReqVO.setBusinessId(id);
+        createReqVO.setBusinessId2(id);
+        createReqVO.setType(BpmBusinessTypeEnum.CHECK_IN_WAIT.getValue());
+        BpmBusinessFormCreateReqVO businessFormCreateReqVO = BeanUtils.toBean(createReqVO, BpmBusinessFormCreateReqVO.class);
+        return bpmBusinessFormService.create(businessFormCreateReqVO);
+    }
 }

+ 2 - 2
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/business/ModelConfigServiceImpl.java

@@ -85,9 +85,9 @@ public class ModelConfigServiceImpl implements ModelConfigService {
     public Integer getModelStatus(String modelKey) {
         ModelConfigDO modelConfigDO = modelConfigMapper.selectOne(new LambdaQueryWrapperX<ModelConfigDO>()
                 .eq(ModelConfigDO::getModelKey, modelKey)
-                .eq(ModelConfigDO::getTenantId, TenantContextHolder.getTenantId())
+                .eqIfPresent(ModelConfigDO::getTenantId, TenantContextHolder.getTenantId())
                 .eq(ModelConfigDO::getDeleted, 0));
-        if (null == modelConfigDO) {
+        if (modelConfigDO == null) {
             return 1;
         }
         return modelConfigDO.getStatus();

+ 45 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/business/listener/BpmCheckInWaitStatusListener.java

@@ -0,0 +1,45 @@
+package cn.iocoder.yudao.module.bpm.service.business.listener;
+
+import cn.iocoder.yudao.module.bpm.dal.dataobject.business.BpmBusinessFormDO;
+import cn.iocoder.yudao.module.bpm.enums.BpmBusinessTypeEnum;
+import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceStatusEnum;
+import cn.iocoder.yudao.module.bpm.event.BpmProcessInstanceStatusEvent;
+import cn.iocoder.yudao.module.bpm.event.BpmProcessInstanceStatusEventListener;
+import cn.iocoder.yudao.module.bpm.service.business.BpmBusinessFormService;
+import cn.iocoder.yudao.module.system.api.bpm.BpmCheckInWaitApi;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+
+/**
+ * 长者轮候入住流程状态监听器
+ */
+@Component
+public class BpmCheckInWaitStatusListener extends BpmProcessInstanceStatusEventListener {
+
+    @Resource
+    private BpmBusinessFormService businessFormService;
+
+    @Resource
+    private BpmCheckInWaitApi checkInWaitApi;
+
+    @Override
+    protected String getProcessDefinitionKey() {
+        return BpmBusinessTypeEnum.CHECK_IN_WAIT.getKey();
+    }
+
+    @Override
+    protected void onEvent(BpmProcessInstanceStatusEvent event) {
+        BpmBusinessFormDO businessFormDO = businessFormService.getBusinessFormByInstanceId(event.getId());
+        businessFormDO.setStatus(event.getStatus());
+        businessFormService.updateBusinessForm(businessFormDO);
+
+        if (BpmProcessInstanceStatusEnum.APPROVE.getStatus().equals(event.getStatus())) {
+            checkInWaitApi.updateCheckInWaitStatus(businessFormDO.getBusinessId(), 10);
+            BpmBusinessFormDO updateDO = new BpmBusinessFormDO();
+            updateDO.setId(businessFormDO.getId());
+            updateDO.setCompleteStatus(1);
+            businessFormService.updateBusinessForm(updateDO);
+        }
+    }
+}

+ 49 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/business/listener/BpmOutboundRegistrationStatusListener.java

@@ -0,0 +1,49 @@
+package cn.iocoder.yudao.module.bpm.service.business.listener;
+
+import cn.iocoder.yudao.module.bpm.dal.dataobject.business.BpmBusinessFormDO;
+import cn.iocoder.yudao.module.bpm.enums.BpmBusinessTypeEnum;
+import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceStatusEnum;
+import cn.iocoder.yudao.module.bpm.event.BpmProcessInstanceStatusEvent;
+import cn.iocoder.yudao.module.bpm.event.BpmProcessInstanceStatusEventListener;
+import cn.iocoder.yudao.module.bpm.service.business.BpmBusinessFormService;
+import cn.iocoder.yudao.module.system.api.bpm.BpmAskLeaveApi;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+
+@Component
+public class BpmOutboundRegistrationStatusListener extends BpmProcessInstanceStatusEventListener {
+
+    @Resource
+    private BpmBusinessFormService businessFormService;
+
+    @Resource
+    private BpmAskLeaveApi askLeaveApi;
+
+    @Override
+    protected String getProcessDefinitionKey() {
+        return BpmBusinessTypeEnum.OUTBOUND_REGISTRATION.getKey();
+    }
+
+    @Override
+    protected void onEvent(BpmProcessInstanceStatusEvent event) {
+        BpmBusinessFormDO businessFormDO = businessFormService.getBusinessFormByInstanceId(event.getId());
+        businessFormDO.setStatus(event.getStatus());
+        businessFormService.updateBusinessForm(businessFormDO);
+
+        if (BpmProcessInstanceStatusEnum.APPROVE.getStatus().equals(event.getStatus())) {
+            askLeaveApi.updateLeaveStatus(businessFormDO.getBusinessId(), 1);
+        } else if (BpmProcessInstanceStatusEnum.REJECT.getStatus().equals(event.getStatus())) {
+            askLeaveApi.updateLeaveStatus(businessFormDO.getBusinessId(), 2);
+        } else if (BpmProcessInstanceStatusEnum.CANCEL.getStatus().equals(event.getStatus())) {
+            askLeaveApi.updateLeaveStatus(businessFormDO.getBusinessId(), 3);
+        }
+
+        if (BpmProcessInstanceStatusEnum.isProcessEndStatus(event.getStatus())) {
+            BpmBusinessFormDO updateDO = new BpmBusinessFormDO();
+            updateDO.setId(businessFormDO.getId());
+            updateDO.setCompleteStatus(1);
+            businessFormService.updateBusinessForm(updateDO);
+        }
+    }
+}

+ 2 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmFormServiceImpl.java

@@ -6,6 +6,7 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
 import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
+import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
 import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.form.BpmFormPageReqVO;
 import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.form.BpmFormSaveReqVO;
 import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmFormDO;
@@ -39,6 +40,7 @@ public class BpmFormServiceImpl implements BpmFormService {
         this.validateFields(createReqVO.getFields());
         // 插入
         BpmFormDO form = BeanUtils.toBean(createReqVO, BpmFormDO.class);
+        form.setTenantId(TenantContextHolder.getTenantId());
         formMapper.insert(form);
         // 返回
         return form.getId();

+ 101 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/message/BpmMessageServiceImpl.java

@@ -28,6 +28,7 @@ import cn.iocoder.yudao.module.bpm.service.message.dto.BpmMessageSendWhenTaskTim
 import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService;
 import cn.iocoder.yudao.module.bpm.service.task.BpmTaskService;
 import cn.iocoder.yudao.module.system.api.bpm.BpmCheckInApi;
+import cn.iocoder.yudao.module.system.api.bpm.BpmCheckInWaitApi;
 import cn.iocoder.yudao.module.system.api.bpm.BpmElderlyApi;
 import cn.iocoder.yudao.module.system.api.bpm.BpmElderlyChangeApi;
 import cn.iocoder.yudao.module.system.api.bpm.vo.ElderlyInfoReqVO;
@@ -44,6 +45,7 @@ import cn.iocoder.yudao.module.system.api.wechat.dto.WechatMessageSaveReq;
 import cn.iocoder.yudao.module.system.enums.notice.NotieBusinessTypeEnum;
 import cn.iocoder.yudao.module.system.enums.notice.NotifyPriorityEnum;
 import cn.iocoder.yudao.module.system.enums.notice.NotifyTypeEnum;
+import com.alibaba.fastjson.JSONObject;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import lombok.extern.slf4j.Slf4j;
 import org.flowable.engine.HistoryService;
@@ -89,6 +91,8 @@ public class BpmMessageServiceImpl implements BpmMessageService {
 
     @Resource
     private BpmElderlyApi bpmElderlyApi;
+    @Autowired
+    private BpmCheckInWaitApi bpmCheckInWaitApi;
 
     @Resource
     private BpmElderlyChangeApi bpmElderlyChangeApi;
@@ -296,6 +300,18 @@ public class BpmMessageServiceImpl implements BpmMessageService {
                         NotieBusinessTypeEnum.BPM.getValue(), NotifyTypeEnum.NOTIFY_TODO.getValue(),
                         NotifyPriorityEnum.MEDIUM.getValue(), 0, assigneeUser.getTenantId());
                 break;
+            case OUTBOUND_REGISTRATION:
+                elderlyInfo = bpmElderlyApi.getElderlyInfo(businessFormDO.getBusinessId2());
+                templateParams.put("nickname", user.getNickname());
+                templateParams.put("elderName", elderlyInfo.getElderName());
+                detailUrl = getProcessInstanceDetailUrl(reqDTO.getProcessInstanceId()) + "&businessId=" + businessFormDO.getBusinessId();
+                templateParams.put("detailUrl", detailUrl);
+                notifySendSingleToUserReqDTO = BpmMessageConvert.INSTANCE.messageConvert(reqDTO.getStartUserId(),
+                        reqDTO.getProcessInstanceId(), "", detailUrl,
+                        BpmMessageEnum.PROCESS_INSTANCE_APPROVE.getTemplateCode(), templateParams,
+                        NotieBusinessTypeEnum.BPM.getValue(), NotifyTypeEnum.NOTIFY_TODO.getValue(),
+                        NotifyPriorityEnum.MEDIUM.getValue(), 0, assigneeUser.getTenantId());
+                break;
             case BED_CHANGE:
                 elderlyInfo = bpmElderlyChangeApi.getElderlyInfoByChangeId(businessFormDO.getBusinessId());
                 templateParams.put("nickname", user.getNickname());
@@ -368,10 +384,25 @@ public class BpmMessageServiceImpl implements BpmMessageService {
                         NotieBusinessTypeEnum.BPM.getValue(), NotifyTypeEnum.NOTIFY_TODO.getValue(),
                         NotifyPriorityEnum.MEDIUM.getValue(), 0, assigneeUser.getTenantId());
                 break;
+            case CHECK_IN_WAIT:
+                JSONObject checkInWaitInfo = bpmCheckInWaitApi.selectById(businessFormDO.getBusinessId2());
+                templateParams.put("nickname", user.getNickname());
+                templateParams.put("elderName", checkInWaitInfo.getString("elderName"));
+                detailUrl = getProcessInstanceDetailUrl(reqDTO.getProcessInstanceId()) + "&businessId=" + businessFormDO.getBusinessId();
+                templateParams.put("detailUrl", detailUrl);
+                notifySendSingleToUserReqDTO = BpmMessageConvert.INSTANCE.messageConvert(reqDTO.getStartUserId(),
+                        reqDTO.getProcessInstanceId(), "", detailUrl,
+                        BpmMessageEnum.CHECK_IN_WAIT_PROCESS_INSTANCE_APPROVE.getTemplateCode(), templateParams,
+                        NotieBusinessTypeEnum.BPM.getValue(), NotifyTypeEnum.NOTIFY_TODO.getValue(),
+                        NotifyPriorityEnum.MEDIUM.getValue(), 0, assigneeUser.getTenantId());
+                break;
             default:
                 break;
 
         }
+        if (notifySendSingleToUserReqDTO == null) {
+            return;
+        }
         String corpUserId = assigneeUser.getCorpUserId() == null ? "" : assigneeUser.getCorpUserId().toString();
         CorpWechatTextcard corpTextCard = new CorpWechatTextcard();
         corpTextCard.setTitle(CorpMsgUtil.TITLE_APPROVE);
@@ -509,6 +540,18 @@ public class BpmMessageServiceImpl implements BpmMessageService {
                         NotieBusinessTypeEnum.BPM.getValue(), notifyType,
                         NotifyPriorityEnum.MEDIUM.getValue(), 0, assigneeUser.getTenantId());
                 break;
+            case OUTBOUND_REGISTRATION:
+                elderlyInfo = bpmElderlyApi.getElderlyInfo(businessFormDO.getBusinessId2());
+                templateParams.put("nickname", user.getNickname());
+                templateParams.put("elderName", elderlyInfo.getElderName());
+                detailUrl = getInstanceDetailUrl(reqDTO.getProcessInstanceId()) + "&businessId=" + businessFormDO.getBusinessId();
+                templateParams.put("detailUrl", detailUrl);
+                notifySendSingleToUserReqDTO = BpmMessageConvert.INSTANCE.messageConvert(reqDTO.getStartUserId(),
+                        reqDTO.getProcessInstanceId(), "", detailUrl,
+                        BpmMessageEnum.PROCESS_INSTANCE_REJECT.getTemplateCode(), templateParams,
+                        NotieBusinessTypeEnum.BPM.getValue(), notifyType,
+                        NotifyPriorityEnum.MEDIUM.getValue(), 0, assigneeUser.getTenantId());
+                break;
             case BED_CHANGE:
                 detailUrl = getInstanceDetailUrl(reqDTO.getProcessInstanceId()) + "&businessId=" + businessFormDO.getBusinessId();
                 elderlyInfo = bpmElderlyChangeApi.getElderlyInfoByChangeId(businessFormDO.getBusinessId());
@@ -580,9 +623,24 @@ public class BpmMessageServiceImpl implements BpmMessageService {
                         NotieBusinessTypeEnum.BPM.getValue(), notifyType,
                         NotifyPriorityEnum.MEDIUM.getValue(), 0, assigneeUser.getTenantId());
                 break;
+            case CHECK_IN_WAIT:
+                detailUrl = getInstanceDetailUrl(reqDTO.getProcessInstanceId()) + "&businessId=" + businessFormDO.getBusinessId();
+                JSONObject checkInWaitInfo = bpmCheckInWaitApi.selectById(businessFormDO.getBusinessId2());
+                templateParams.put("nickname", user.getNickname());
+                templateParams.put("elderName", checkInWaitInfo.getString("elderName"));
+                templateParams.put("detailUrl", detailUrl);
+                notifySendSingleToUserReqDTO = BpmMessageConvert.INSTANCE.messageConvert(reqDTO.getStartUserId(),
+                        reqDTO.getProcessInstanceId(), "", detailUrl,
+                        BpmMessageEnum.CHECK_IN_WAIT_PROCESS_INSTANCE_REJECT.getTemplateCode(), templateParams,
+                        NotieBusinessTypeEnum.BPM.getValue(), notifyType,
+                        NotifyPriorityEnum.MEDIUM.getValue(), 0, assigneeUser.getTenantId());
+                break;
             default:
                 break;
         }
+        if (notifySendSingleToUserReqDTO == null) {
+            return;
+        }
         String corpUserId = assigneeUser.getCorpUserId() == null ? "" : assigneeUser.getCorpUserId().toString();
         CorpWechatTextcard corpTextCard = new CorpWechatTextcard();
         corpTextCard.setTitle(CorpMsgUtil.TITLE_REFUSE);
@@ -783,6 +841,46 @@ public class BpmMessageServiceImpl implements BpmMessageService {
             }
         }
 
+        if (BpmBusinessTypeEnum.CHECK_IN_WAIT.getValue().equals(businessFormDO.getType())) {
+            JSONObject checkInWaitInfo = bpmCheckInWaitApi.selectById(businessFormDO.getBusinessId2());
+            templateParams.put("nickname", user.getNickname());
+            templateParams.put("elderName", checkInWaitInfo.getString("elderName"));
+            detailUrl = getInstanceDetailUrl(reqDTO.getProcessInstanceId()) + "&businessId=" + businessFormDO.getBusinessId();
+            templateParams.put("detailUrl", detailUrl);
+            notifySendSingleToUserReqDTO = BpmMessageConvert.INSTANCE.messageConvert(reqDTO.getAssigneeUserId(),
+                    reqDTO.getProcessInstanceId(), reqDTO.getTaskId(), detailUrl,
+                    BpmMessageEnum.CHECK_IN_WAIT_PROCESS_TASK_ASSIGNED.getTemplateCode(), templateParams,
+                    NotieBusinessTypeEnum.BPM.getValue(), NotifyTypeEnum.NOTIFY_TODO.getValue(),
+                    NotifyPriorityEnum.MEDIUM.getValue(), 0, assigneeUser.getTenantId());
+            if (reqDTO.getStartUserId().equals(loginUserId)) {
+                notifyMessageSendApi.sendSingleMessageToAdminByDTO(BpmMessageConvert.INSTANCE.messageConvert(loginUserId,
+                        reqDTO.getProcessInstanceId(), reqDTO.getTaskId(), detailUrl,
+                        BpmMessageEnum.CHECK_IN_WAIT_PROCESS_TASK_ASSIGNED.getTemplateCode(), templateParams,
+                        NotieBusinessTypeEnum.BPM.getValue(), NotifyTypeEnum.NOTIFY_TODO.getValue(),
+                        NotifyPriorityEnum.MEDIUM.getValue(), 1, assigneeUser.getTenantId()));
+            }
+        }
+
+        if (BpmBusinessTypeEnum.OUTBOUND_REGISTRATION.getValue().equals(businessFormDO.getType())) {
+            elderlyInfo = bpmElderlyApi.getElderlyInfo(businessFormDO.getBusinessId2());
+            templateParams.put("nickname", user.getNickname());
+            templateParams.put("elderName", elderlyInfo.getElderName());
+            detailUrl = getInstanceDetailUrl(reqDTO.getProcessInstanceId()) + "&businessId=" + businessFormDO.getBusinessId();
+            templateParams.put("detailUrl", detailUrl);
+            notifySendSingleToUserReqDTO = BpmMessageConvert.INSTANCE.messageConvert(reqDTO.getAssigneeUserId(),
+                    reqDTO.getProcessInstanceId(), reqDTO.getTaskId(), detailUrl,
+                    BpmMessageEnum.TASK_ASSIGNED.getTemplateCode(), templateParams,
+                    NotieBusinessTypeEnum.BPM.getValue(), NotifyTypeEnum.NOTIFY_TODO.getValue(),
+                    NotifyPriorityEnum.MEDIUM.getValue(), 0, assigneeUser.getTenantId());
+            if (reqDTO.getStartUserId().equals(loginUserId)) {
+                notifyMessageSendApi.sendSingleMessageToAdminByDTO(BpmMessageConvert.INSTANCE.messageConvert(loginUserId,
+                        reqDTO.getProcessInstanceId(), reqDTO.getTaskId(), detailUrl,
+                        BpmMessageEnum.TASK_ASSIGNED.getTemplateCode(), templateParams,
+                        NotieBusinessTypeEnum.BPM.getValue(), NotifyTypeEnum.NOTIFY_TODO.getValue(),
+                        NotifyPriorityEnum.MEDIUM.getValue(), 1, assigneeUser.getTenantId()));
+            }
+        }
+
         if (null == approveMethod && startNodeFlag) {
             elderlyInfo = bpmElderlyApi.getElderlyInfo(businessFormDO.getBusinessId2());
             templateParams.put("nickname", user.getNickname());
@@ -798,6 +896,9 @@ public class BpmMessageServiceImpl implements BpmMessageService {
             return;
         }
 
+        if (notifySendSingleToUserReqDTO == null) {
+            return;
+        }
         String corpUserId = assigneeUser.getCorpUserId() == null ? "" : assigneeUser.getCorpUserId().toString();
         CorpWechatTextcard corpTextCard = new CorpWechatTextcard();
         corpTextCard.setTitle(CorpMsgUtil.TITLE_HANDLED);

+ 14 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceService.java

@@ -5,6 +5,7 @@ import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO;
 import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.elderly.BedChangePageReqVO;
 import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.elderly.BpmElderlyInfoVO;
 import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.elderly.CheckInPageReqVO;
+import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.elderly.CheckInWaitPageReqVO;
 import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.elderly.ElderlyRetreatRecordPageReqVO;
 import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.*;
 import cn.iocoder.yudao.module.system.api.bpm.vo.*;
@@ -181,8 +182,21 @@ public interface BpmProcessInstanceService {
 
     PageResult<CheckInRecordRespVO> getProcessInstanceCheckInPage(Long loginUserId, CheckInPageReqVO pageReqVO);
 
+    PageResult<CheckInWaitRecordRespVO> getProcessInstanceCheckInWaitPage(Long loginUserId, CheckInWaitPageReqVO pageReqVO);
+
     BpmMyPageCountRespVO getBpmListCount(Long loginUserId, Integer value, Integer queryType);
 
+    /**
+     * 获得指定租户下、停留时间超过超时天数的流程实例列表
+     * <p>
+     * 仅统计“进行中”的流程(即当前停留在某个审批节点),且在该节点的停留时间已超过传入的超时天数
+     *
+     * @param tenantId     租户编号
+     * @param timeoutDays  超时天数(天)
+     * @return 超时流程实例列表,每条包含:长者姓名、流程类型、到达该节点的时间、审核人
+     */
+    List<BpmProcessOverdueRespVO> getOverdueProcessInstanceList(Long tenantId, Integer timeoutDays);
+
     void deleteAllHistoricProcessInstance();
 
     PageResult<BpmProcessInstanceRespVO> getDoneProcessInstancePage(Long loginUserId, BpmProcessInstancePageReqVO pageVO);

+ 121 - 1
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java

@@ -23,6 +23,7 @@ import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.B
 import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.elderly.BedChangePageReqVO;
 import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.elderly.BpmElderlyInfoVO;
 import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.elderly.CheckInPageReqVO;
+import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.elderly.CheckInWaitPageReqVO;
 import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.elderly.ElderlyRetreatRecordPageReqVO;
 import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.*;
 import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmApprovalDetailRespVO.ActivityNodeTask;
@@ -33,6 +34,7 @@ import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmCategoryDO;
 import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO;
 import cn.iocoder.yudao.module.bpm.dal.mysql.business.BpmBusinessFormMapper;
 import cn.iocoder.yudao.module.bpm.dal.redis.BpmProcessIdRedisDAO;
+import cn.iocoder.yudao.module.bpm.enums.BpmBusinessTypeEnum;
 import cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants;
 import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelTypeEnum;
 import cn.iocoder.yudao.module.bpm.enums.definition.BpmSimpleModelNodeTypeEnum;
@@ -53,6 +55,7 @@ import cn.iocoder.yudao.module.bpm.service.definition.BpmCategoryService;
 import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService;
 import cn.iocoder.yudao.module.bpm.service.message.BpmMessageService;
 import cn.iocoder.yudao.module.system.api.bpm.BpmCheckInApi;
+import cn.iocoder.yudao.module.system.api.bpm.BpmCheckInWaitApi;
 import cn.iocoder.yudao.module.system.api.bpm.BpmElderlyApi;
 import cn.iocoder.yudao.module.system.api.bpm.BpmElderlyChangeApi;
 import cn.iocoder.yudao.module.system.api.bpm.vo.*;
@@ -75,6 +78,7 @@ import org.flowable.engine.history.HistoricProcessInstanceQuery;
 import org.flowable.engine.repository.ProcessDefinition;
 import org.flowable.engine.runtime.ProcessInstance;
 import org.flowable.engine.runtime.ProcessInstanceBuilder;
+import org.flowable.task.api.Task;
 import org.flowable.task.api.TaskQuery;
 import org.flowable.task.api.history.HistoricTaskInstance;
 import org.flowable.task.api.history.HistoricTaskInstanceQuery;
@@ -162,6 +166,9 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
     @Resource
     private BpmCheckInApi checkInApi;
 
+    @Resource
+    private BpmCheckInWaitApi checkInWaitApi;
+
     @Resource
     private BpmElderlyChangeApi elderlyChangeApi;
 
@@ -774,7 +781,7 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
     private void validateStartUserSelectAssignees(ProcessDefinition definition, Map<String, List<Long>> startUserSelectAssignees) {
         // 1. 获得发起人自选审批人的 UserTask/ServiceTask 列表
         BpmnModel bpmnModel = processDefinitionService.getProcessDefinitionBpmnModel(definition.getId());
-        List<Task> tasks = BpmTaskCandidateStartUserSelectStrategy.getStartUserSelectTaskList(bpmnModel);
+        List<org.flowable.bpmn.model.Task> tasks = BpmTaskCandidateStartUserSelectStrategy.getStartUserSelectTaskList(bpmnModel);
         if (CollUtil.isEmpty(tasks)) {
             return;
         }
@@ -820,6 +827,11 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
         updateProcessInstanceCancel(cancelReqVO.getId(),
                 BpmReasonEnum.CANCEL_PROCESS_INSTANCE_BY_START_USER.format(cancelReqVO.getReason()));
 
+        BpmBusinessFormDO businessFormDO = businessFormService.selectOneByProcessInstanceId(instance.getProcessInstanceId());
+        if (businessFormDO != null && Objects.equals(businessFormDO.getType(), BpmBusinessTypeEnum.CHECK_IN.getValue())) {
+            checkInApi.unbindBedOnCancel(businessFormDO.getBusinessId());
+        }
+
         // 取消完成全部消息
         notifyMessageSendApi.updateSelfMessageStatus(instance.getProcessInstanceId(), "", null, null,
                 BpmMessageStatusEnum.COMPLETE.getValue(), null,  Boolean.FALSE);
@@ -882,6 +894,9 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
                         BpmProcessInstanceConvert.INSTANCE.buildProcessInstanceRejectMessage(instance, reason));
             }
             BpmBusinessFormDO businessFormDO = businessFormService.selectOneByProcessInstanceId(instance.getProcessInstanceId());
+            if (businessFormDO == null) {
+                return;
+            }
             // 3. 发送流程实例的状态事件
             processInstanceEventPublisher.sendProcessInstanceResultEvent(
                     BpmProcessInstanceConvert.INSTANCE.buildProcessInstanceStatusEvent(this, instance, status, businessFormDO.getStartTenantId(), 0));
@@ -1201,6 +1216,51 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
         return pageResult;
     }
 
+    @Override
+    public PageResult<CheckInWaitRecordRespVO> getProcessInstanceCheckInWaitPage(Long loginUserId, CheckInWaitPageReqVO pageReqVO) {
+        CheckInWaitApiPageReqVO apiVO = BeanUtils.toBean(pageReqVO, CheckInWaitApiPageReqVO.class);
+        PageResult<CheckInWaitRecordRespVO> pageResult = checkInWaitApi.getProcessInstanceCheckInWaitPage(apiVO);
+        if (pageResult.getList().isEmpty()) {
+            return PageResult.empty();
+        }
+        List<CheckInWaitRecordRespVO> list = pageResult.getList();
+        Set<String> processInstanceIds = new HashSet<>();
+        list.forEach(item -> processInstanceIds.add(item.getProcessInstanceId()));
+
+        List<HistoricProcessInstance> processInstanceList = historyService.createHistoricProcessInstanceQuery()
+                .processInstanceIds(processInstanceIds).list();
+        processInstanceList.forEach(processInstance -> {
+            list.forEach(record -> {
+                if (processInstance.getId().equals(record.getProcessInstanceId())) {
+                    record.setStartTaskId(processInstance.getId());
+                }
+            });
+        });
+
+        // 补充当前审批人:取 /bpm/task/list-by-process-instance-id 逻辑中的最后一个任务的 assigneeUser
+        list.forEach(record -> {
+            List<HistoricTaskInstance> taskList = bpmTaskService.getTaskListByProcessInstanceId(record.getProcessInstanceId(), true);
+            HistoricTaskInstance latestTask = CollUtil.getLast(taskList);
+            if (latestTask == null || StrUtil.isBlank(latestTask.getAssignee())) {
+                return;
+            }
+            Long assigneeUserId = Long.valueOf(latestTask.getAssignee());
+            AdminUserRespDTO assigneeUser = adminUserApi.getUserTenantIgnore(assigneeUserId);
+            if (assigneeUser == null) {
+                return;
+            }
+            CheckInWaitRecordRespVO.ApproverUser approverUser = new CheckInWaitRecordRespVO.ApproverUser();
+            approverUser.setId(assigneeUser.getId());
+            approverUser.setNickname(assigneeUser.getNickname());
+            approverUser.setDeptId(assigneeUser.getDeptId());
+            DeptRespDTO dept = assigneeUser.getDeptId() == null ? null : deptApi.getDeptTenantIgnore(assigneeUser.getDeptId());
+            approverUser.setDeptName(dept == null ? null : dept.getName());
+            record.setAssigneeUser(approverUser);
+        });
+
+        return pageResult;
+    }
+
     @Override
     @TenantIgnore
     public BpmMyPageCountRespVO getBpmListCount(Long userId, Integer value, Integer queryType) {
@@ -1227,6 +1287,66 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
         return bpmMyPageCountRespVO;
     }
 
+    @Override
+    @TenantIgnore
+    public List<BpmProcessOverdueRespVO> getOverdueProcessInstanceList(Long tenantId, Integer timeoutDays) {
+        // 1. 查询该租户下所有进行中的任务(即流程当前停留的节点)
+        List<Task> tasks = taskService.createTaskQuery()
+                .active()
+                .taskTenantId(String.valueOf(tenantId))
+                .orderByTaskCreateTime().asc()
+                .list();
+        if (CollUtil.isEmpty(tasks)) {
+            return Collections.emptyList();
+        }
+        // 2. 过滤出停留时间超过超时天数的任务:到达节点时间 + 超时天数 < 当前时间
+        Date deadline = DateUtil.offsetDay(new Date(), -timeoutDays);
+        List<Task> overdueTasks = filterList(tasks, task -> task.getCreateTime().before(deadline));
+        if (CollUtil.isEmpty(overdueTasks)) {
+            return Collections.emptyList();
+        }
+        // 3. 批量补充长者信息(用于过滤长者姓名为空、以及轮候入住流程)
+        Set<String> processInstanceIds = convertSet(overdueTasks, Task::getProcessInstanceId);
+        Map<String, BpmElderlyInfoVO> elderMap = businessFormService.getElderMapByProcessInstanceId(processInstanceIds);
+        // 3.1 过滤:长者姓名为空,或业务类型为轮候入住(check_in_wait)的不查询
+        overdueTasks = filterList(overdueTasks, task -> {
+            BpmElderlyInfoVO elderlyInfoVO = elderMap.get(task.getProcessInstanceId());
+            if (elderlyInfoVO == null || StrUtil.isBlank(elderlyInfoVO.getElderName())) {
+                return false;
+            }
+            return !BpmBusinessTypeEnum.CHECK_IN_WAIT.getValue().equals(elderlyInfoVO.getType());
+        });
+        if (CollUtil.isEmpty(overdueTasks)) {
+            return Collections.emptyList();
+        }
+        // 4. 批量补充流程定义(流程类型)、审核人
+        Set<String> processDefinitionIds = convertSet(overdueTasks, Task::getProcessDefinitionId);
+        Map<String, ProcessDefinition> processDefinitionMap = processDefinitionService.getProcessDefinitionMap(processDefinitionIds);
+        Set<Long> assigneeIds = new HashSet<>();
+        overdueTasks.forEach(task -> {
+            if (StrUtil.isNotBlank(task.getAssignee())) {
+                assigneeIds.add(Long.valueOf(task.getAssignee()));
+            }
+        });
+        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(assigneeIds);
+        // 4. 拼接返回
+        return convertList(overdueTasks, task -> {
+            BpmProcessOverdueRespVO respVO = new BpmProcessOverdueRespVO();
+            respVO.setProcessInstanceId(task.getProcessInstanceId());
+            respVO.setTaskName(task.getName());
+            respVO.setArriveTime(DateUtils.of(task.getCreateTime()));
+            ProcessDefinition definition = processDefinitionMap.get(task.getProcessDefinitionId());
+            respVO.setProcessType(definition == null ? null : definition.getName());
+            BpmElderlyInfoVO elderlyInfoVO = elderMap.get(task.getProcessInstanceId());
+            respVO.setElderName(elderlyInfoVO == null ? null : elderlyInfoVO.getElderName());
+            if (StrUtil.isNotBlank(task.getAssignee())) {
+                AdminUserRespDTO user = userMap.get(Long.valueOf(task.getAssignee()));
+                respVO.setReviewer(user == null ? null : user.getNickname());
+            }
+            return respVO;
+        });
+    }
+
     @Override
     public void deleteAllHistoricProcessInstance() {
         List<HistoricProcessInstance> historicProcessInstances = historyService.createHistoricProcessInstanceQuery()

+ 233 - 4
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java

@@ -9,6 +9,7 @@ import cn.iocoder.yudao.framework.common.util.date.DateUtils;
 import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
 import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
 import cn.iocoder.yudao.framework.common.util.object.PageUtils;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
 import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
 import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
 import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
@@ -16,8 +17,13 @@ import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.elderly.BpmElderlyIn
 import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.elderly.ElderlyLogRecordVO;
 import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.*;
 import cn.iocoder.yudao.module.bpm.convert.task.BpmTaskConvert;
+import cn.iocoder.yudao.module.bpm.dal.dataobject.business.BpmBusinessFormDO;
 import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmFormDO;
 import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO;
+import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmTaskCopyRelationDO;
+import cn.iocoder.yudao.module.bpm.dal.mysql.definition.BpmTaskCopyRelationMapper;
+import cn.iocoder.yudao.module.bpm.enums.BpmBusinessTypeEnum;
+import cn.iocoder.yudao.module.bpm.enums.BpmCheckInWaitStatusEnum;
 import cn.iocoder.yudao.module.bpm.enums.definition.*;
 import cn.iocoder.yudao.module.bpm.enums.task.BpmCommentTypeEnum;
 import cn.iocoder.yudao.module.bpm.enums.task.BpmReasonEnum;
@@ -35,7 +41,9 @@ import cn.iocoder.yudao.module.bpm.service.message.BpmMessageService;
 import cn.iocoder.yudao.module.bpm.service.message.dto.BpmMessageSendWhenTaskTimeoutReqDTO;
 import cn.iocoder.yudao.module.system.api.dept.DeptApi;
 import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
+import cn.iocoder.yudao.module.system.api.bpm.BpmCheckInWaitApi;
 import cn.iocoder.yudao.module.system.api.bpm.BpmElderlyApi;
+import cn.iocoder.yudao.module.system.api.bpm.vo.CheckInWaitSupplementReqVO;
 import cn.iocoder.yudao.module.system.api.notify.NotifyMessageSendApi;
 import cn.iocoder.yudao.module.system.api.tenant.TenantApi;
 import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
@@ -133,6 +141,9 @@ public class BpmTaskServiceImpl implements BpmTaskService {
     @Resource
     private BpmElderlyApi bpmElderlyApi;
 
+    @Resource
+    private BpmCheckInWaitApi checkInWaitApi;
+
     @Resource
     private NotifyMessageSendApi notifyMessageSendApi;
 
@@ -142,6 +153,9 @@ public class BpmTaskServiceImpl implements BpmTaskService {
     @Autowired
     private RequestScopedStorage requestScopedStorage;
 
+    @Autowired
+    private BpmTaskCopyRelationMapper bpmTaskCopyRelationMapper;
+
     // ========== Query 查询相关方法 ==========
 
     @Override
@@ -161,6 +175,14 @@ public class BpmTaskServiceImpl implements BpmTaskService {
             taskQuery.taskCreatedAfter(DateUtils.of(pageVO.getCreateTime()[0]));
             taskQuery.taskCreatedBefore(DateUtils.of(pageVO.getCreateTime()[1]));
         }
+        // 按标题(templateContent)/长者姓名模糊匹配过滤:长者姓名存储在业务表单,需先按流程实例维度过滤
+        if (StrUtil.isNotBlank(pageVO.getTemplateContent())) {
+            Set<String> matchedProcessInstanceIds = getMatchedProcessInstanceIdsByTemplateContent(userId, pageVO.getTemplateContent());
+            if (CollUtil.isEmpty(matchedProcessInstanceIds)) {
+                return PageResult.empty();
+            }
+            taskQuery.processInstanceIdIn(matchedProcessInstanceIds);
+        }
         long count = taskQuery.count();
         if (count == 0) {
             return PageResult.empty();
@@ -186,6 +208,37 @@ public class BpmTaskServiceImpl implements BpmTaskService {
         return bpmTaskRespVOPageResult;
     }
 
+    /**
+     * 根据标题(templateContent)/长者姓名模糊匹配,过滤出符合条件的流程实例ID集合
+     * <p>
+     * 长者姓名存储在业务表单中(通过 tenantMap 获取),Flowable 的 TaskQuery 无法直接过滤,
+     * 故先查出当前用户全部待办任务对应的流程实例,按长者姓名模糊匹配后再用 processInstanceIdIn 约束分页查询。
+     *
+     * @param userId          当前用户ID
+     * @param templateContent 模糊匹配关键字(长者姓名)
+     * @return 匹配到的流程实例ID集合
+     */
+    private Set<String> getMatchedProcessInstanceIdsByTemplateContent(Long userId, String templateContent) {
+        List<Task> allTasks = taskService.createTaskQuery()
+                .taskAssignee(String.valueOf(userId))
+                .active()
+                .list();
+        if (CollUtil.isEmpty(allTasks)) {
+            return Collections.emptySet();
+        }
+        Set<String> allProcessInstanceIds = convertSet(allTasks, Task::getProcessInstanceId);
+        Map<String, BpmTaskRespVO> tenantMap = businessFormService.getTenantMap(allProcessInstanceIds);
+        if (CollUtil.isEmpty(tenantMap)) {
+            return Collections.emptySet();
+        }
+        String keyword = templateContent.trim();
+        return tenantMap.values().stream()
+                .filter(vo -> StrUtil.isNotBlank(vo.getElderName()) && vo.getElderName().contains(keyword))
+                .map(BpmTaskRespVO::getProcessInstanceId)
+                .filter(StrUtil::isNotBlank)
+                .collect(Collectors.toSet());
+    }
+
     @Override
     public BpmTaskRespVO getFirstTodoTask(Long userId, String processInstanceId) {
         if (processInstanceId == null) {
@@ -601,10 +654,16 @@ public class BpmTaskServiceImpl implements BpmTaskService {
                 BpmCommentTypeEnum.APPROVE.formatComment(reqVO.getReason()));
         // 2.3 调用 BPM complete 去完成任务
         // 其中,variables 是存储动态表单到 local 任务级别。过滤一下,避免 ProcessInstance 系统级的变量被占用
+        Map<String, Object> variables = null;
         if (CollUtil.isNotEmpty(reqVO.getVariables())) {
-            Map<String, Object> variables = FlowableUtils.filterTaskFormVariable(reqVO.getVariables());
+            variables = FlowableUtils.filterTaskFormVariable(reqVO.getVariables());
             // 修改表单的值需要存储到 ProcessInstance 变量
             runtimeService.setVariables(task.getProcessInstanceId(), variables);
+        }
+        // 轮候入住流程在审批通过时,根据节点+变量补充业务字段
+        handleCheckInWaitApprove(task.getProcessInstanceId(), task.getTaskDefinitionKey(), variables);
+
+        if (CollUtil.isNotEmpty(variables)) {
             taskService.complete(task.getId(), variables, true);
         } else {
             taskService.complete(task.getId());
@@ -632,11 +691,131 @@ public class BpmTaskServiceImpl implements BpmTaskService {
         }
         LogRecordContext.putVariable("nickname", SecurityFrameworkUtils.getLoginUserNickname());
         LogRecordContext.putVariable("instanceName", instance.getName());
-
+        Set<Long> copyUserIdList = getCopyUserIdList(task.getTaskDefinitionKey());
         if (CollUtil.isNotEmpty(reqVO.getCopyUserIds())) {
+            copyUserIdList.addAll(reqVO.getCopyUserIds());
             //processInstanceCopyService.createProcessInstanceCopy(reqVO.getCopyUserIds(), reqVO.getId());
-            processInstanceCopyService.createProcessInstanceCopy(reqVO.getCopyUserIds(), task);
         }
+        processInstanceCopyService.createProcessInstanceCopy(copyUserIdList, task);
+    }
+
+    /**
+     * 轮候入住流程审批通过时,根据审批节点补充业务数据
+     */
+    private void handleCheckInWaitApprove(String processInstanceId, String taskDefinitionKey, Map<String, Object> variables) {
+        BpmBusinessFormDO businessForm = businessFormService.selectOneByProcessInstanceId(processInstanceId);
+        if (businessForm == null || !BpmBusinessTypeEnum.CHECK_IN_WAIT.getValue().equals(businessForm.getType())) {
+            return;
+        }
+        if (StrUtil.isBlank(taskDefinitionKey)) {
+            return;
+        }
+        CheckInWaitSupplementReqVO reqVO = new CheckInWaitSupplementReqVO();
+        reqVO.setId(businessForm.getBusinessId());
+        String key = taskDefinitionKey.toLowerCase();
+        if ("first_approve".equals(key)) {
+            reqVO.setIsPsychosis("否");
+            checkInWaitApi.updateFirstReviewInfo(reqVO);
+            checkInWaitApi.updateCheckInWaitStatusByBusinessId(businessForm.getBusinessId(),
+                    BpmCheckInWaitStatusEnum.FIRST_APPROVAL_PASSED_WAIT_SECOND_APPROVAL.getStatus());
+            return;
+        }
+        if ("second_approve".equals(key)) {
+            reqVO.setFirstAuditName(parseString(variables, "firstAuditName"));
+            reqVO.setMedicalCareRemark(parseString(variables, "medicalCareRemark"));
+            if (StrUtil.isNotBlank(reqVO.getFirstAuditName()) || StrUtil.isNotBlank(reqVO.getMedicalCareRemark())) {
+                checkInWaitApi.updateSecondReviewInfo(reqVO);
+            }
+            checkInWaitApi.updateCheckInWaitStatusByBusinessId(businessForm.getBusinessId(),
+                    BpmCheckInWaitStatusEnum.SECOND_APPROVAL_PASSED_WAIT_THIRD_APPROVAL.getStatus());
+            return;
+        }
+        if ("third_approve".equals(key)) {
+            reqVO.setNurseLevelId(parseLong(variables, "nurseLevelId"));
+            if (reqVO.getNurseLevelId() != null || StrUtil.isNotBlank(reqVO.getRemark())) {
+                checkInWaitApi.updateThirdReviewInfo(reqVO);
+            }
+            checkInWaitApi.updateCheckInWaitStatusByBusinessId(businessForm.getBusinessId(),
+                    BpmCheckInWaitStatusEnum.THIRD_APPROVAL_PASSED_WAIT_FOURTH_APPROVAL.getStatus());
+            return;
+        }
+        if ("fourth_approve".equals(key)) {
+            reqVO.setPhysicalExaminationReport(parseString(variables, "physicalExaminationReport"));
+            reqVO.setRemark(parseString(variables, "remark"));
+            reqVO.setSecondLiverTwoHalfAndHalf(parseString(variables, "secondLiverTwoHalfAndHalf"));
+            reqVO.setSyphilis(parseString(variables, "syphilis"));
+            reqVO.setHiv(parseString(variables, "hiv"));
+            reqVO.setTuberculosisBacillusAntibody(parseString(variables, "tuberculosisBacillusAntibody"));
+            reqVO.setHepatitisC(parseString(variables, "hepatitisC"));
+            reqVO.setHepatitisE(parseString(variables, "hepatitisE"));
+            reqVO.setBloodRoutineExamination(parseString(variables, "bloodRoutineExamination"));
+            reqVO.setUrineRoutines(parseString(variables, "urineRoutines"));
+            reqVO.setFecalRoutine(parseString(variables, "fecalRoutine"));
+            reqVO.setLiverFunction(parseString(variables, "liverFunction"));
+            reqVO.setBloodBiochemistry(parseString(variables, "bloodBiochemistry"));
+            reqVO.setRabat(parseString(variables, "rabat"));
+            reqVO.setHeadCt(parseString(variables, "headCt"));
+            reqVO.setEcg(parseString(variables, "ecg"));
+            reqVO.setBmd(parseString(variables, "bmd"));
+            reqVO.setCognitiveAssessmentReport(parseString(variables, "cognitiveAssessmentReport"));
+            if (StrUtil.isNotBlank(reqVO.getPhysicalExaminationReport())
+                    || StrUtil.isNotBlank(reqVO.getSecondLiverTwoHalfAndHalf())
+                    || StrUtil.isNotBlank(reqVO.getSyphilis())
+                    || StrUtil.isNotBlank(reqVO.getHiv())
+                    || StrUtil.isNotBlank(reqVO.getTuberculosisBacillusAntibody())
+                    || StrUtil.isNotBlank(reqVO.getHepatitisC())
+                    || StrUtil.isNotBlank(reqVO.getHepatitisE())
+                    || StrUtil.isNotBlank(reqVO.getBloodRoutineExamination())
+                    || StrUtil.isNotBlank(reqVO.getUrineRoutines())
+                    || StrUtil.isNotBlank(reqVO.getFecalRoutine())
+                    || StrUtil.isNotBlank(reqVO.getLiverFunction())
+                    || StrUtil.isNotBlank(reqVO.getBloodBiochemistry())
+                    || StrUtil.isNotBlank(reqVO.getRabat())
+                    || StrUtil.isNotBlank(reqVO.getHeadCt())
+                    || StrUtil.isNotBlank(reqVO.getEcg())
+                    || StrUtil.isNotBlank(reqVO.getBmd())
+                    || StrUtil.isNotBlank(reqVO.getCognitiveAssessmentReport())) {
+                checkInWaitApi.updateFourthReviewInfo(reqVO);
+            }
+            checkInWaitApi.updateCheckInWaitStatusByBusinessId(businessForm.getBusinessId(),
+                    BpmCheckInWaitStatusEnum.FOURTH_APPROVAL_PASSED_WAIT_FIFTH_APPROVAL.getStatus());
+            return;
+        }
+        if ("fifth_approve".equals(key)) {
+            reqVO.setAuditName(parseString(variables, "auditName"));
+            if (StrUtil.isNotBlank(reqVO.getAuditName())) {
+                checkInWaitApi.updateFifthReviewInfo(reqVO);
+            }
+            checkInWaitApi.updateCheckInWaitStatusByBusinessId(businessForm.getBusinessId(),
+                    BpmCheckInWaitStatusEnum.FOURTH_APPROVAL_PASSED_WAIT_FIFTH_APPROVAL.getStatus());
+            return;
+        }
+        if ("sixth_approve".equals(key)) {
+            reqVO.setBedId(parseLong(variables, "bedId"));
+            if (reqVO.getBedId() != null) {
+                checkInWaitApi.updateSixthReviewInfo(reqVO);
+            }
+            checkInWaitApi.updateCheckInWaitStatusByBusinessId(businessForm.getBusinessId(),
+                    BpmCheckInWaitStatusEnum.APPROVED_FINISHED.getStatus());
+        }
+    }
+
+    private Long parseLong(Map<String, Object> variables, String key) {
+        if (variables == null || variables.get(key) == null) {
+            return null;
+        }
+        Object value = variables.get(key);
+        if (value instanceof Number) {
+            return ((Number) value).longValue();
+        }
+        return NumberUtils.parseLong(String.valueOf(value));
+    }
+
+    private String parseString(Map<String, Object> variables, String key) {
+        if (variables == null || variables.get(key) == null) {
+            return null;
+        }
+        return String.valueOf(variables.get(key));
     }
 
     /**
@@ -772,6 +951,9 @@ public class BpmTaskServiceImpl implements BpmTaskService {
         LogRecordContext.putVariable("nickname", SecurityFrameworkUtils.getLoginUserNickname());
         LogRecordContext.putVariable("instanceName", instance.getName());
 
+        // 2.4 轮候入住流程,按驳回节点回写业务状态(11/12/13/14)
+        updateCheckInWaitRejectStatus(task.getProcessInstanceId(), task.getTaskDefinitionKey());
+
         // 3. 根据不同的 RejectHandler 处理策略
         BpmnModel bpmnModel = modelService.getBpmnModelByDefinitionId(task.getProcessDefinitionId());
         FlowElement userTaskElement = BpmnModelUtils.getFlowElementById(bpmnModel, task.getTaskDefinitionKey());
@@ -789,6 +971,41 @@ public class BpmTaskServiceImpl implements BpmTaskService {
         moveTaskToEnd(task.getProcessInstanceId(), BpmCommentTypeEnum.REJECT.formatComment(reqVO.getReason())); // 结束流程
     }
 
+    /**
+     * 回写轮候入住驳回状态
+     */
+    private void updateCheckInWaitRejectStatus(String processInstanceId, String taskDefinitionKey) {
+        BpmBusinessFormDO businessForm = businessFormService.selectOneByProcessInstanceId(processInstanceId);
+        if (businessForm == null || !BpmBusinessTypeEnum.CHECK_IN_WAIT.getValue().equals(businessForm.getType())) {
+            return;
+        }
+        Integer status = resolveCheckInWaitRejectStatus(taskDefinitionKey);
+        if (status != null) {
+            checkInWaitApi.updateCheckInWaitStatusByBusinessId(businessForm.getBusinessId(), status);
+        }
+    }
+
+    /**
+     * 驳回节点 -> 轮候状态映射
+     */
+    private Integer resolveCheckInWaitRejectStatus(String taskDefinitionKey) {
+        if (StrUtil.isBlank(taskDefinitionKey)) {
+            return null;
+        }
+        switch (taskDefinitionKey) {
+            case "first_approve":
+                return BpmCheckInWaitStatusEnum.FIRST_APPROVAL_REJECT_TO_FAMILY.getStatus();
+            case "second_approve":
+                return BpmCheckInWaitStatusEnum.SECOND_APPROVAL_REJECT_FINISHED.getStatus();
+            case "fifth_approve":
+                return BpmCheckInWaitStatusEnum.FIFTH_APPROVAL_REJECT_FINISHED.getStatus();
+            case "sixth_approve":
+                return BpmCheckInWaitStatusEnum.SIXTH_APPROVAL_REJECT_TO_FIFTH.getStatus();
+            default:
+                return BpmCheckInWaitStatusEnum.SECOND_APPROVAL_REJECT_FINISHED.getStatus();
+        }
+    }
+
     /**
      * 更新流程任务的 status 状态
      *
@@ -1559,5 +1776,17 @@ public class BpmTaskServiceImpl implements BpmTaskService {
 
         return map;
     }
-
+    private Set<Long> getCopyUserIdList(String taskDefinitionKey){
+        Set<Long> result = new HashSet<>();
+        List<BpmTaskCopyRelationDO> bpmTaskCopyRelationList = bpmTaskCopyRelationMapper.selectList(new LambdaQueryWrapperX<BpmTaskCopyRelationDO>()
+                .eq(BpmTaskCopyRelationDO::getTaskDefinitionKey, taskDefinitionKey));
+        for (BpmTaskCopyRelationDO bpmTaskCopyRelationDO : bpmTaskCopyRelationList) {
+            result.addAll(Arrays.stream(bpmTaskCopyRelationDO.getUserIdList().split(","))
+                    .map(String::trim)
+                    .filter(s -> !s.isEmpty())
+                    .map(Long::parseLong)
+                    .collect(Collectors.toSet()));
+        }
+        return result;
+    }
 }

+ 17 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/resources/mapper/BpmBusinessFormMapper.xml

@@ -293,6 +293,23 @@
         <foreach collection="processInstanceIds" item="processInstanceId" open="(" separator="," close=")">
             #{processInstanceId}
         </foreach>
+
+        UNION ALL
+
+        SELECT
+        CONCAT(st.id, '') AS tenantId,
+        st.name AS tenantName,
+        ciw.elder_name,
+        bbf.process_instance_id
+        FROM
+        bpm_business_form bbf
+        INNER JOIN elderly_check_in_wait ciw ON bbf.business_id2 = ciw.id
+        INNER JOIN system_tenant st ON ciw.tenant_id = st.id
+        WHERE
+        bbf.process_instance_id IN
+        <foreach collection="processInstanceIds" item="processInstanceId" open="(" separator="," close=")">
+            #{processInstanceId}
+        </foreach>
     </select>
 
     <select id="getReadList" resultType="cn.iocoder.yudao.module.bpm.controller.admin.task.vo.elderly.BpmElderlyInfoVO">

+ 9 - 1
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileController.java

@@ -6,6 +6,7 @@ import cn.hutool.core.util.URLUtil;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
 import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.*;
 import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO;
 import cn.iocoder.yudao.module.infra.service.file.FileService;
@@ -47,10 +48,17 @@ public class FileController {
     @PostMapping("/upload")
     @Operation(summary = "上传文件", description = "模式一:后端上传文件")
     @LogRecord(type = UPLOAD_FILE_TYPE,subType = UPLOAD_FILE,success = UPLOAD_FILE_SUCCESS, bizNo = "{{#file.size}}",extra = "{{#uploadReqVO.elderId}}" )
+    @PermitAll
+    @TenantIgnore
     public CommonResult<String> uploadFile(FileUploadReqVO uploadReqVO) throws Exception {
         MultipartFile file = uploadReqVO.getFile();
         String path = uploadReqVO.getPath();
-        String result = fileService.createFile(file.getOriginalFilename(), path, IoUtil.readBytes(file.getInputStream()));
+        String result = "";
+        try{
+            result = fileService.createFile(file.getOriginalFilename(), path, IoUtil.readBytes(file.getInputStream()));
+        }catch (Exception e){
+            log.error("upload异常:,{}", (Object) e.getStackTrace());
+        }
         LogRecordContext.putVariable("file",file);
         return success(result);
     }

+ 198 - 37
yudao-module-report/yudao-module-report-biz/src/main/java/cn/iocoder/yudao/module/report/controller/admin/goview/GoViewDataController.java

@@ -9,6 +9,7 @@ import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
 import cn.iocoder.yudao.module.report.controller.admin.goview.vo.data.GoViewDataGetBySqlReqVO;
 import cn.iocoder.yudao.module.report.controller.admin.goview.vo.data.GoViewDataRespVO;
 import cn.iocoder.yudao.module.system.api.order.OrderApi;
+import cn.iocoder.yudao.module.system.api.sys.BusinessParamsConfigApi;
 import cn.iocoder.yudao.module.system.api.order.VO.OrderItemRespVO;
 import cn.iocoder.yudao.module.system.api.order.VO.OrderItemTotalRespVO;
 import cn.iocoder.yudao.module.system.api.order.VO.OrderRespVO;
@@ -30,6 +31,7 @@ import javax.annotation.Resource;
 import javax.validation.Valid;
 
 import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.TimeUnit;
 
 import java.math.BigDecimal;
@@ -49,6 +51,8 @@ public class GoViewDataController {
 
     private static final int SCALE_TWO = 2;
 
+    private static final int SCALE_ZERO = 0;
+
     private static BigDecimal scale2(BigDecimal v) {
         if (v == null) {
             return null;
@@ -102,11 +106,142 @@ public class GoViewDataController {
     @Resource
     private OrderApi orderApi;
 
+    @Resource
+    private BusinessParamsConfigApi businessParamsConfigApi;
+
     @Resource
     private CacheManager cacheManager;
 
     private static final String ORDER_SUB_CACHE_NAME = "report:goview:orderSubGrouped#30s";
 
+    /**
+     * 账单金额单项取整设置:配置所属标题
+     */
+    private static final String AMOUNT_ROUND_CONFIG_TITLE = "财务模块配置";
+    /**
+     * 账单金额单项取整设置:配置名称
+     */
+    private static final String AMOUNT_ROUND_CONFIG_NAME = "账单金额单项取整设置";
+    /**
+     * 配置值:保留两位小数(四舍五入)
+     */
+    private static final String AMOUNT_ROUND_VALUE_TWO_SCALE = "4";
+    /**
+     * 配置值:四舍五入取整
+     */
+    private static final String AMOUNT_ROUND_VALUE_INTEGER = "2";
+
+    /**
+     * 按照机构的「账单金额单项取整设置」配置,对金额做小数位处理
+     *
+     * @param v           金额
+     * @param configValue 配置值:4-保留两位小数四舍五入;2-四舍五入取整;其它-保持现状
+     */
+    private static BigDecimal roundByConfig(BigDecimal v, String configValue) {
+        if (v == null) {
+            return null;
+        }
+        if (AMOUNT_ROUND_VALUE_TWO_SCALE.equals(configValue)) {
+            return v.setScale(SCALE_TWO, RoundingMode.HALF_UP);
+        }
+        if (AMOUNT_ROUND_VALUE_INTEGER.equals(configValue)) {
+            return v.setScale(0, RoundingMode.HALF_UP);
+        }
+        return v;
+    }
+
+    private static void roundOrderItemByConfig(OrderItemRespVO vo, String configValue) {
+        if (vo == null) {
+            return;
+        }
+        vo.setBedAmount(roundByConfig(vo.getBedAmount(), configValue));
+        vo.setMealAmount(roundByConfig(vo.getMealAmount(), configValue));
+        vo.setNurseAmount(roundByConfig(vo.getNurseAmount(), configValue));
+        vo.setServiceAmount(roundByConfig(vo.getServiceAmount(), configValue));
+        vo.setExtraServiceAmount(roundByConfig(vo.getExtraServiceAmount(), configValue));
+        vo.setTotalAmount(roundByConfig(vo.getTotalAmount(), configValue));
+
+        vo.setAdjustAmount(roundByConfig(vo.getAdjustAmount(), configValue));
+
+        vo.setBedActualAmount(roundByConfig(vo.getBedActualAmount(), configValue));
+        vo.setMealActualAmount(roundByConfig(vo.getMealActualAmount(), configValue));
+        vo.setNurseActualAmount(roundByConfig(vo.getNurseActualAmount(), configValue));
+        vo.setServiceActualAmount(roundByConfig(vo.getServiceActualAmount(), configValue));
+        vo.setTotalActualAmount(roundByConfig(vo.getTotalActualAmount(), configValue));
+    }
+
+    private static void roundOrderTotalByConfig(OrderItemTotalRespVO vo, String configValue) {
+        if (vo == null) {
+            return;
+        }
+        vo.setBedTotalAmount(roundByConfig(vo.getBedTotalAmount(), configValue));
+        vo.setMealTotalAmount(roundByConfig(vo.getMealTotalAmount(), configValue));
+        vo.setNurseTotalAmount(roundByConfig(vo.getNurseTotalAmount(), configValue));
+        vo.setServiceTotalAmount(roundByConfig(vo.getServiceTotalAmount(), configValue));
+        vo.setExtraServiceAmount(roundByConfig(vo.getExtraServiceAmount(), configValue));
+        vo.setTotalAmount(roundByConfig(vo.getTotalAmount(), configValue));
+
+        vo.setAdjustTotalAmount(roundByConfig(vo.getAdjustTotalAmount(), configValue));
+
+        vo.setBedActualTotalAmount(roundByConfig(vo.getBedActualTotalAmount(), configValue));
+        vo.setMealActualTotalAmount(roundByConfig(vo.getMealActualTotalAmount(), configValue));
+        vo.setNurseActualTotalAmount(roundByConfig(vo.getNurseActualTotalAmount(), configValue));
+        vo.setServiceActualTotalAmount(roundByConfig(vo.getServiceActualTotalAmount(), configValue));
+        vo.setTotalActualAmount(roundByConfig(vo.getTotalActualAmount(), configValue));
+    }
+
+    /**
+     * 「账单金额单项取整设置」配置值缓存:配置为低频变更数据,避免每个报表接口都查库
+     */
+    private static final long AMOUNT_ROUND_CONFIG_CACHE_TTL_MS = 5 * 60 * 1000L;
+    private static final Map<Long, AmountRoundConfigCacheEntry> AMOUNT_ROUND_CONFIG_CACHE = new ConcurrentHashMap<>();
+
+    private static class AmountRoundConfigCacheEntry {
+        private final String value;
+        private final long expireAt;
+
+        AmountRoundConfigCacheEntry(String value, long expireAt) {
+            this.value = value;
+            this.expireAt = expireAt;
+        }
+    }
+
+    /**
+     * 获取当前机构的「账单金额单项取整设置」配置值(本地短期缓存)
+     */
+    private String getAmountRoundConfigValue(Long tenantId) {
+        if (tenantId == null) {
+            return null;
+        }
+        long now = System.currentTimeMillis();
+        AmountRoundConfigCacheEntry entry = AMOUNT_ROUND_CONFIG_CACHE.get(tenantId);
+        if (entry != null && entry.expireAt > now) {
+            return entry.value;
+        }
+        String value;
+        try {
+            value = businessParamsConfigApi.getConfigValue(tenantId, AMOUNT_ROUND_CONFIG_TITLE, AMOUNT_ROUND_CONFIG_NAME);
+        } catch (Exception e) {
+            // 配置获取失败时,保持现状,不影响报表查询;不缓存,下次继续尝试
+            return null;
+        }
+        AMOUNT_ROUND_CONFIG_CACHE.put(tenantId, new AmountRoundConfigCacheEntry(value, now + AMOUNT_ROUND_CONFIG_CACHE_TTL_MS));
+        return value;
+    }
+
+    /**
+     * 配置值转换为小数保留位数:4-保留两位小数;2-四舍五入取整;其它-不做处理
+     */
+    private static Integer toRoundScale(String configValue) {
+        if (AMOUNT_ROUND_VALUE_TWO_SCALE.equals(configValue)) {
+            return SCALE_TWO;
+        }
+        if (AMOUNT_ROUND_VALUE_INTEGER.equals(configValue)) {
+            return SCALE_ZERO;
+        }
+        return null;
+    }
+
     private OrderSubListGroupedRespVO loadOrComputeGrouped(Long tenantId, String billingMonth, Integer payStatus) {
         String cacheKey = tenantId + "|" + billingMonth + "|" + (payStatus == null ? "null" : payStatus);
 
@@ -118,13 +253,16 @@ public class GoViewDataController {
             }
         }
 
-        List<OrderItemRespVO> listByMonth = orderApi.getOrderSubListForReport(null, tenantId, billingMonth, payStatus);
+        List<OrderItemRespVO> listByMonth = orderApi.getOrderSubListForReport(1, tenantId, billingMonth, payStatus);
         OrderSubListGroupedRespVO grouped = new OrderSubListGroupedRespVO();
         if (!CollectionUtils.isEmpty(listByMonth)) {
+            // 按当前机构的「账单金额单项取整设置」配置,处理金额小数位
+            String roundConfigValue = getAmountRoundConfigValue(tenantId);
             List<OrderItemRespVO> leave = new ArrayList<>();
             List<OrderItemRespVO> retreat = new ArrayList<>();
             List<OrderItemRespVO> in = new ArrayList<>();
             for (OrderItemRespVO vo : listByMonth) {
+                roundOrderItemByConfig(vo, roundConfigValue);
                 String status = vo.getInStatus();
                 if ("离住".equals(status)) {
                     leave.add(vo);
@@ -212,9 +350,7 @@ public class GoViewDataController {
         String billingMonth = StringUtils.isBlank(params.get("billingMonth")) ? "2025-12" : params.get("billingMonth");
         Integer payStatus = params.get("payStatus") == null ? null : Integer.valueOf(params.get("payStatus"));
         OrderSubListGroupedRespVO grouped = loadOrComputeGrouped(getTenantId(params), billingMonth, payStatus);
-        if (!CollectionUtils.isEmpty(grouped.getInList())) {
-            grouped.getInList().forEach(GoViewDataController::scale2OrderItem);
-        }
+        // 金额小数位已在 loadOrComputeGrouped 中按机构配置处理
         return success(grouped.getInList());
     }
 
@@ -227,9 +363,7 @@ public class GoViewDataController {
         String billingMonth = StringUtils.isBlank(params.get("billingMonth")) ? "2025-12" : params.get("billingMonth");
         Integer payStatus = params.get("payStatus") == null ? null : Integer.valueOf(params.get("payStatus"));
         OrderSubListGroupedRespVO grouped = loadOrComputeGrouped(getTenantId(params), billingMonth, payStatus);
-        if (!CollectionUtils.isEmpty(grouped.getLeaveList())) {
-            grouped.getLeaveList().forEach(GoViewDataController::scale2OrderItem);
-        }
+        // 金额小数位已在 loadOrComputeGrouped 中按机构配置处理
         return success(grouped.getLeaveList());
     }
 
@@ -242,9 +376,7 @@ public class GoViewDataController {
         String billingMonth = StringUtils.isBlank(params.get("billingMonth")) ? "2025-12" : params.get("billingMonth");
         Integer payStatus = params.get("payStatus") == null ? null : Integer.valueOf(params.get("payStatus"));
         OrderSubListGroupedRespVO grouped = loadOrComputeGrouped(getTenantId(params), billingMonth, payStatus);
-        if (!CollectionUtils.isEmpty(grouped.getRetreatList())) {
-            grouped.getRetreatList().forEach(GoViewDataController::scale2OrderItem);
-        }
+        // 金额小数位已在 loadOrComputeGrouped 中按机构配置处理
         return success(grouped.getRetreatList());
     }
 
@@ -254,9 +386,14 @@ public class GoViewDataController {
             @RequestParam(required = false) Map<String, String> params,
             @RequestBody(required = false) String body) { // params、body 按照需要去接收,这里仅仅是示例
         String billingMonth = params.get("billingMonth");
+        Long tenantId = getTenantId(params);
         Integer payStatus = params.get("payStatus") == null ? null : Integer.valueOf(params.get("payStatus"));
-        OrderItemTotalRespVO orderItemTotalRespVO = orderApi.getTotalByMonth(null, getTenantId(params), billingMonth,payStatus);
-        scale2OrderTotal(orderItemTotalRespVO);
+        // 配置了小数保留位数时:由取数侧先对每项金额处理小数,再进行汇总(一次取数完成,不再二次取数)
+        Integer roundScale = toRoundScale(getAmountRoundConfigValue(tenantId));
+        OrderItemTotalRespVO orderItemTotalRespVO = orderApi.getTotalByMonth(1, tenantId, billingMonth, payStatus, roundScale);
+        if (roundScale == null) {
+            scale2OrderTotal(orderItemTotalRespVO);
+        }
         List<OrderItemTotalRespVO> list = new ArrayList<>();
         list.add(orderItemTotalRespVO);
         return success(list);
@@ -286,9 +423,14 @@ public class GoViewDataController {
         String billingMonth = params.get("billingMonth");
         List<OrderItemTotalRespVO> list = new ArrayList<>();
         if (null == billingMonth) return success(list);
+        Long tenantId = getTenantId(params);
         Integer payStatus = params.get("payStatus") == null ? null : Integer.valueOf(params.get("payStatus"));
-        OrderItemTotalRespVO orderItemTotalRespVO = orderApi.getOrderReceivableTotal(null, getTenantId(params), billingMonth,payStatus);
-        scale2OrderTotal(orderItemTotalRespVO);
+        // 配置了小数保留位数时:由取数侧先对每项金额处理小数,再进行汇总(复用原有取数,不重复遍历月份)
+        Integer roundScale = toRoundScale(getAmountRoundConfigValue(tenantId));
+        OrderItemTotalRespVO orderItemTotalRespVO = orderApi.getOrderReceivableTotal(null, tenantId, billingMonth, payStatus, roundScale);
+        if (roundScale == null) {
+            scale2OrderTotal(orderItemTotalRespVO);
+        }
         list.add(orderItemTotalRespVO);
         return success(list);
     }
@@ -297,65 +439,84 @@ public class GoViewDataController {
     // 新增:仅汇总在住(本地缓存聚合)
     @RequestMapping("/getOrderSubTotalIn")
     @Operation(summary = "查询在住汇总", description = "仅汇总在住记录 (本地缓存聚合)")
+    @TenantIgnore
     public CommonResult<List<OrderItemTotalRespVO>> getOrderSubTotalIn(
             @RequestParam(required = false) Map<String, String> params,
             @RequestBody(required = false) String body) {
         String billingMonth = params.get("billingMonth");
         Integer payStatus = params.get("payStatus") == null ? null : Integer.valueOf(params.get("payStatus"));
-        OrderSubListGroupedRespVO grouped = loadOrComputeGrouped(getTenantId(params), billingMonth, payStatus);
-        OrderItemTotalRespVO total = aggregate(grouped.getInList(), billingMonth);
-        scale2OrderTotal(total);
+        Long tenantId = getTenantId(params);
+        OrderSubListGroupedRespVO grouped = loadOrComputeGrouped(tenantId, billingMonth, payStatus);
+        // 先对每项金额处理小数,再进行汇总
+        String roundConfigValue = getAmountRoundConfigValue(tenantId);
+        OrderItemTotalRespVO total = aggregate(grouped.getInList(), billingMonth, roundConfigValue);
+        roundOrderTotalByConfig(total, roundConfigValue);
         return success(java.util.Collections.singletonList(total));
     }
 
     // 新增:仅汇总离住
     @RequestMapping("/getOrderSubTotalLeave")
     @Operation(summary = "查询离住汇总", description = "仅汇总离住记录 (本地缓存聚合)")
+    @TenantIgnore
     public CommonResult<List<OrderItemTotalRespVO>> getOrderSubTotalLeave(
             @RequestParam(required = false) Map<String, String> params,
             @RequestBody(required = false) String body) {
         String billingMonth = params.get("billingMonth");
         Integer payStatus = params.get("payStatus") == null ? null : Integer.valueOf(params.get("payStatus"));
-        OrderSubListGroupedRespVO grouped = loadOrComputeGrouped(getTenantId(params), billingMonth, payStatus);
-        OrderItemTotalRespVO total = aggregate(grouped.getLeaveList(), billingMonth);
-        scale2OrderTotal(total);
+        Long tenantId = getTenantId(params);
+        OrderSubListGroupedRespVO grouped = loadOrComputeGrouped(tenantId, billingMonth, payStatus);
+        // 先对每项金额处理小数,再进行汇总
+        String roundConfigValue = getAmountRoundConfigValue(tenantId);
+        OrderItemTotalRespVO total = aggregate(grouped.getLeaveList(), billingMonth, roundConfigValue);
+        roundOrderTotalByConfig(total, roundConfigValue);
         return success(java.util.Collections.singletonList(total));
     }
 
     // 新增:仅汇总退住
     @RequestMapping("/getOrderSubTotalRetreat")
     @Operation(summary = "查询退住汇总", description = "仅汇总退住记录 (本地缓存聚合)")
+    @TenantIgnore
     public CommonResult<List<OrderItemTotalRespVO>> getOrderSubTotalRetreat(
             @RequestParam(required = false) Map<String, String> params,
             @RequestBody(required = false) String body) {
         String billingMonth = params.get("billingMonth");
         Integer payStatus = params.get("payStatus") == null ? null : Integer.valueOf(params.get("payStatus"));
-        OrderSubListGroupedRespVO grouped = loadOrComputeGrouped(getTenantId(params), billingMonth, payStatus);
-        OrderItemTotalRespVO total = aggregate(grouped.getRetreatList(), billingMonth);
-        scale2OrderTotal(total);
+        Long tenantId = getTenantId(params);
+        OrderSubListGroupedRespVO grouped = loadOrComputeGrouped(tenantId, billingMonth, payStatus);
+        // 先对每项金额处理小数,再进行汇总
+        String roundConfigValue = getAmountRoundConfigValue(tenantId);
+        OrderItemTotalRespVO total = aggregate(grouped.getRetreatList(), billingMonth, roundConfigValue);
+        roundOrderTotalByConfig(total, roundConfigValue);
         return success(java.util.Collections.singletonList(total));
     }
 
-    // Helper:本地聚合
-    private OrderItemTotalRespVO aggregate(List<OrderItemRespVO> src, String billingMonth) {
+    /**
+     * Helper:本地聚合
+     * 如果配置了小数保留位数(roundConfigValue),则先对每项金额做小数处理,再进行汇总,
+     * 保证「汇总金额 = 各项金额之和」(而不是先汇总再对合计做小数处理)
+     */
+    private OrderItemTotalRespVO aggregate(List<OrderItemRespVO> src, String billingMonth, String roundConfigValue) {
         OrderItemTotalRespVO t = new OrderItemTotalRespVO();
         t.setBillingMonth(billingMonth);
         if (src == null || src.isEmpty()) {
             return t;
         }
         for (OrderItemRespVO vo : src) {
-            t.setBedTotalAmount(t.getBedTotalAmount().add(nz(vo.getBedAmount())));
-            t.setMealTotalAmount(t.getMealTotalAmount().add(nz(vo.getMealAmount())));
-            t.setNurseTotalAmount(t.getNurseTotalAmount().add(nz(vo.getNurseAmount())));
-            t.setServiceTotalAmount(t.getServiceTotalAmount().add(nz(vo.getServiceAmount())));
-            t.setTotalAmount(t.getTotalAmount().add(nz(vo.getTotalAmount())));
-            t.setAdjustTotalAmount(t.getAdjustTotalAmount().add(nz(vo.getAdjustAmount())));
-            t.setBedActualTotalAmount(t.getBedActualTotalAmount().add(nz(vo.getBedActualAmount())));
-            t.setMealActualTotalAmount(t.getMealActualTotalAmount().add(nz(vo.getMealActualAmount())));
-            t.setNurseActualTotalAmount(t.getNurseActualTotalAmount().add(nz(vo.getNurseActualAmount())));
-            t.setServiceActualTotalAmount(t.getServiceActualTotalAmount().add(nz(vo.getServiceActualAmount())));
-            t.setTotalActualAmount(t.getTotalActualAmount().add(nz(vo.getTotalActualAmount())));
-            t.setExtraServiceAmount(t.getExtraServiceAmount().add(nz(vo.getExtraServiceAmount())));
+            if (vo == null) {
+                continue;
+            }
+            t.setBedTotalAmount(t.getBedTotalAmount().add(nz(roundByConfig(vo.getBedAmount(), roundConfigValue))));
+            t.setMealTotalAmount(t.getMealTotalAmount().add(nz(roundByConfig(vo.getMealAmount(), roundConfigValue))));
+            t.setNurseTotalAmount(t.getNurseTotalAmount().add(nz(roundByConfig(vo.getNurseAmount(), roundConfigValue))));
+            t.setServiceTotalAmount(t.getServiceTotalAmount().add(nz(roundByConfig(vo.getServiceAmount(), roundConfigValue))));
+            t.setTotalAmount(t.getTotalAmount().add(nz(roundByConfig(vo.getTotalAmount(), roundConfigValue))));
+            t.setAdjustTotalAmount(t.getAdjustTotalAmount().add(nz(roundByConfig(vo.getAdjustAmount(), roundConfigValue))));
+            t.setBedActualTotalAmount(t.getBedActualTotalAmount().add(nz(roundByConfig(vo.getBedActualAmount(), roundConfigValue))));
+            t.setMealActualTotalAmount(t.getMealActualTotalAmount().add(nz(roundByConfig(vo.getMealActualAmount(), roundConfigValue))));
+            t.setNurseActualTotalAmount(t.getNurseActualTotalAmount().add(nz(roundByConfig(vo.getNurseActualAmount(), roundConfigValue))));
+            t.setServiceActualTotalAmount(t.getServiceActualTotalAmount().add(nz(roundByConfig(vo.getServiceActualAmount(), roundConfigValue))));
+            t.setTotalActualAmount(t.getTotalActualAmount().add(nz(roundByConfig(vo.getTotalActualAmount(), roundConfigValue))));
+            t.setExtraServiceAmount(t.getExtraServiceAmount().add(nz(roundByConfig(vo.getExtraServiceAmount(), roundConfigValue))));
         }
         if (t.getTotalAmount().compareTo(java.math.BigDecimal.ZERO) > 0) {
             java.math.BigDecimal p = t.getTotalActualAmount().divide(t.getTotalAmount(),2,java.math.RoundingMode.HALF_UP).multiply(new java.math.BigDecimal("100"));

+ 6 - 0
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/bpm/BpmAskLeaveApi.java

@@ -0,0 +1,6 @@
+package cn.iocoder.yudao.module.system.api.bpm;
+
+public interface BpmAskLeaveApi {
+
+    void updateLeaveStatus(Long askLeaveId, Integer leaveStatus);
+}

+ 2 - 0
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/bpm/BpmCheckInApi.java

@@ -22,6 +22,8 @@ public interface BpmCheckInApi {
 
     Integer completeCheckIn(Long businessId);
 
+    Integer unbindBedOnCancel(Long businessId);
+
     PageResult<CheckInRecordRespVO> getProcessInstanceCheckInPage(CheckInApiPageReqVO apiVO);
 
     Integer bindBedByType(CheckInCreateReqVO createReqVO, Integer queryType);

+ 36 - 0
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/bpm/BpmCheckInWaitApi.java

@@ -0,0 +1,36 @@
+package cn.iocoder.yudao.module.system.api.bpm;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.system.api.bpm.vo.CheckInWaitApiPageReqVO;
+import cn.iocoder.yudao.module.system.api.bpm.vo.CheckInWaitCreateReqVO;
+import cn.iocoder.yudao.module.system.api.bpm.vo.CheckInWaitRecordRespVO;
+import cn.iocoder.yudao.module.system.api.bpm.vo.CheckInWaitSupplementReqVO;
+import com.alibaba.fastjson.JSONObject;
+
+import java.util.List;
+
+public interface BpmCheckInWaitApi {
+
+    Long createCheckInWait(CheckInWaitCreateReqVO reqVO);
+
+    Integer updateInfo(CheckInWaitSupplementReqVO reqVO);
+
+    Integer updateFirstReviewInfo(CheckInWaitSupplementReqVO reqVO);
+    Integer updateSecondReviewInfo(CheckInWaitSupplementReqVO reqVO);
+
+    Integer updateCheckInWaitStatus(Long id, Integer status);
+
+    Integer updateCheckInWaitStatusByBusinessId(Long businessId, Integer status);
+
+    Integer updateThirdReviewInfo(CheckInWaitSupplementReqVO reqVO);
+
+    Integer updateFourthReviewInfo(CheckInWaitSupplementReqVO reqVO);
+    Integer updateFifthReviewInfo(CheckInWaitSupplementReqVO reqVO);
+
+    Integer updateSixthReviewInfo(CheckInWaitSupplementReqVO reqVO);
+
+    JSONObject selectById(Long id);
+
+
+    PageResult<CheckInWaitRecordRespVO> getProcessInstanceCheckInWaitPage(CheckInWaitApiPageReqVO pageReqVO);
+}

+ 2 - 0
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/bpm/BpmElderlyApi.java

@@ -47,6 +47,8 @@ public interface BpmElderlyApi {
 
     void releaseBed(Long retreatRecordId);
 
+    void revokeApprovedRetreat(Long retreatRecordId);
+
     void delectRetreatRecordByElderId(Long elderId, Integer orgType, Long tenantId);
 
     Long generateApplyAccommodationData(Long elderId, Long startTenantId);

+ 3 - 0
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/bpm/BpmElderlyChangeApi.java

@@ -32,6 +32,9 @@ public interface BpmElderlyChangeApi {
 
     Boolean checkRepeat(Long elderlyId, LocalDate changeDate, Integer changeType, Long tenantId);
 
+    List<ElderlyCurrentChargeItemRespVO> listCurrentChargeItemList(Long tenantId, Integer orgType,
+                                                                   Integer identification, List<Long> overheadChargeIds);
+
     PageResult<NurseChangeRecordRespVO> getNurseChangeRecordPage(ChangeRecordPageReqApiVO apiVO);
 
     PageResult<CateringChangeRecordRespVO> getCateringChangeRecordPage(ChangeRecordPageReqApiVO apiVO);

+ 28 - 1
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/bpm/BpmElderlyExpenseApi.java

@@ -15,11 +15,38 @@ public interface BpmElderlyExpenseApi {
      */
     void generateRefundSettlementOrder(Long businessId, Long startTenantId);
 
+    void revokeApprovedRetreat(Long retreatRecordId);
+
     /**
      * 退住申请前检查
      * 检查是否有未缴费的账单,存在则不能申请抛出异常
      * 检查是否有未进行返院登记的外出记录,存在则不能申请抛出异常
      */
+    void applyRetreatCheck(Long elderId, LocalDate costDeadlineDate);
+
+    /**
+     * 退住申请通用校验(普通退住与离住转退住均需校验)
+     *
+     * @param elderId           长者ID
+     * @param costDeadlineDate 费用截止日期
+     */
+    void applyRetreatCommonCheck(Long elderId, LocalDate costDeadlineDate);
+
+    /**
+     * 退住申请专属校验(仅普通退住需校验,离住转退住跳过)
+     * 检查是否存在未返院的离住记录,存在则不能申请抛出异常
+     *
+     * @param elderId 长者ID
+     */
+    void applyRetreatTempOutCheck(Long elderId);
 
-    void applyRetreatCheck(Long elderId,LocalDate costDeadlineDate);
+    /**
+     * 离住转退住:生成费用截止日期当月的月度账单(若该月不存在时),并对指定离住记录做返院登记
+     *
+     * @param tempOutId        离住记录ID
+     * @param elderId          长者ID
+     * @param tenantId         机构ID
+     * @param costDeadlineDate 费用截止日期(作为返院日期,并据此确定账单月份)
+     */
+    void handleTempOutReturnForRetreat(Long tempOutId, Long elderId, Long tenantId, LocalDate costDeadlineDate);
 }

+ 1 - 0
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/bpm/vo/CheckInExpenseItemReqVo.java

@@ -120,4 +120,5 @@ public class CheckInExpenseItemReqVo {
 
     private Date createTime;
 
+    private Integer isDayCalculate;
 }

+ 32 - 0
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/bpm/vo/CheckInWaitApiPageReqVO.java

@@ -0,0 +1,32 @@
+package cn.iocoder.yudao.module.system.api.bpm.vo;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class CheckInWaitApiPageReqVO extends PageParam {
+
+    /**
+     * 长者姓名
+     */
+    private String elderName;
+
+    /**
+     * 审批状态,1-审批中,2-审批通过,3-审批不通过,4-已取消,5-系统审批
+     */
+    private Integer bpmStatus;
+
+    /**
+     * 轮候入住业务状态,1-14
+     */
+    private Integer status;
+
+    private Integer type;
+
+    private Long[] tenantIds;
+}
+

+ 38 - 0
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/bpm/vo/CheckInWaitCreateReqVO.java

@@ -0,0 +1,38 @@
+package cn.iocoder.yudao.module.system.api.bpm.vo;
+
+import lombok.Data;
+
+@Data
+public class CheckInWaitCreateReqVO {
+
+    private Long id;
+
+    private String elderName;
+
+    private String idCard;
+
+    private String recentLivingSituation;
+
+    private String walkSituationVideo;
+
+    private String medicalRecordsWithinSixMonth;
+
+    private String idCardPhoto;
+
+    private String recentDischargeSummary;
+    private String commonMedicine;
+
+    private Long tenantId;
+
+    private Integer status;
+
+    private Long nurseLevelId;
+
+    private String remark;
+
+    private String physicalExaminationReport;
+
+    private Long bedId;
+    private String commitRemark;
+}
+

+ 45 - 0
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/bpm/vo/CheckInWaitRecordRespVO.java

@@ -0,0 +1,45 @@
+package cn.iocoder.yudao.module.system.api.bpm.vo;
+
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Data
+public class CheckInWaitRecordRespVO {
+
+    private Long id;
+    private String elderName;
+    private String idCard;
+    private String recentLivingSituation;
+    private String walkSituationVideo;
+    private String medicalRecordsWithinSixMonth;
+    private String idCardPhoto;
+    private String recentDischargeSummary;
+    private Long tenantId;
+    private Integer status;
+    private Long nurseLevelId;
+    private String remark;
+    private String physicalExaminationReport;
+    private String firstAuditName;
+    private String auditName;
+    private Long bedId;
+    private Integer bpmStatus;
+    private String processInstanceId;
+    private String startTaskId;
+    /**
+     * 当前审批人,来源于 /bpm/task/list-by-process-instance-id 返回列表最后一个任务的 assigneeUser
+     */
+    private ApproverUser assigneeUser;
+    private Integer type;
+    private String tenantName;
+    private LocalDateTime createdTime;
+
+    @Data
+    public static class ApproverUser {
+        private Long id;
+        private String nickname;
+        private String avatar;
+        private Long deptId;
+        private String deptName;
+    }
+}

+ 39 - 0
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/bpm/vo/CheckInWaitSupplementReqVO.java

@@ -0,0 +1,39 @@
+package cn.iocoder.yudao.module.system.api.bpm.vo;
+
+import lombok.Data;
+
+@Data
+public class CheckInWaitSupplementReqVO {
+
+    private Long id;
+
+    private Long nurseLevelId;
+
+    private String remark;
+    private String medicalCareRemark;
+    private String physicalExaminationReport;
+
+    private String isPsychosis;
+
+    private String firstAuditName;
+    private String auditName;
+
+    private Long bedId;
+
+    private String secondLiverTwoHalfAndHalf;
+    private String syphilis;
+    private String hiv;
+    private String tuberculosisBacillusAntibody;
+    private String hepatitisC;
+    private String hepatitisE;
+    private String bloodRoutineExamination;
+    private String urineRoutines;
+    private String fecalRoutine;
+    private String liverFunction;
+    private String bloodBiochemistry;
+    private String rabat;
+    private String headCt;
+    private String ecg;
+    private String bmd;
+    private String cognitiveAssessmentReport;
+}

+ 1 - 0
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/bpm/vo/ElderlyChangeRecordReqVO.java

@@ -75,6 +75,7 @@ public class ElderlyChangeRecordReqVO {
      * 原始金额
      */
     private BigDecimal originalAmount;
+    private String hysteresisMonth;
 
 
 }

+ 31 - 0
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/bpm/vo/ElderlyCurrentChargeItemRespVO.java

@@ -0,0 +1,31 @@
+package cn.iocoder.yudao.module.system.api.bpm.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Data
+public class ElderlyCurrentChargeItemRespVO {
+
+    private Long elderlyId;
+
+    private String elderlyName;
+
+    private String idCard;
+
+    private Long tenantId;
+
+    private Long expenseItemId;
+
+    private Long overheadChargeId;
+
+    private String itemName;
+
+    private BigDecimal actualAmount;
+
+    private BigDecimal discountAmount;
+
+    private Integer isDiscount;
+
+    private String discount;
+}

+ 0 - 3
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/kingdee/KingdeeApi.java

@@ -14,9 +14,6 @@ public interface KingdeeApi {
     Integer syncArReceivable(Long tenantId, String billingMonth,Long elderId);
     Integer syncArReceivable(Long tenantId, String billingMonth);
 
-    // 收款单
-    Integer syncArReceivebill(Long tenantId);
-
 
     String syncArReceivebillNew(Long expenseOrderId);
     String syncArReceivebillNewBatch(List<Long> expenseOrderId);

+ 4 - 0
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/kingdee/dto/ReceiveBillSaveEntryDTO.java

@@ -109,6 +109,10 @@ public class ReceiveBillSaveEntryDTO {
      * 费用编码
      */
     private KingdeeNumberDTO FCOSTID;
+    /**
+     * 费用编码
+     */
+    private KingdeeNumberDTO F_PAEZ_Base;
 
     /**
      * 备注

+ 1 - 0
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/kingdee/dto/ReceiveBillSaveModelDTO.java

@@ -53,6 +53,7 @@ public class ReceiveBillSaveModelDTO {
 
 
     private String FCancelStatus;
+    private String FREMARK;
 
     private KingdeeNumberDTO FSETTLEMAINBOOKID;
 

+ 2 - 0
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/notify/NotifyMessageSendApi.java

@@ -54,4 +54,6 @@ public interface NotifyMessageSendApi {
     Long sendCorpCancelMessage(CorpSendMessage corpSendMessage);
 
     Integer updateMessageByCodeAndPkMessage(String templateCode, Long elderlyId);
+
+    Integer deleteByPkMessage(String pkMessage);
 }

+ 10 - 1
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/order/DTO/OrderItemRespDTO.java

@@ -6,6 +6,7 @@ import lombok.Data;
 import java.math.BigDecimal;
 import java.time.LocalDate;
 import java.util.Date;
+import java.util.Map;
 
 /**
  * @Author: jinyd
@@ -39,7 +40,7 @@ public class OrderItemRespDTO {
     private String bedName;
 
     // 房间类型
-    private String itemName;
+    private String bedItemName;
 
     // 护理等级
     private String nurseLevelName;
@@ -128,6 +129,14 @@ public class OrderItemRespDTO {
     // 水电费
     private BigDecimal orderWaterElectricityAmount;
 
+    private BigDecimal orderOtherIncomeAmount;
+
+    private Map<String, BigDecimal> orderNursingItemStats;
+
+    private Map<String, BigDecimal> orderServiceItemStats;
+
+    private Map<String, BigDecimal> orderOtherIncomeItemStats;
+
     // 增值服务费
     private BigDecimal extraServiceAmount;
 

+ 22 - 2
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/order/OrderApi.java

@@ -10,11 +10,31 @@ public interface OrderApi {
 
     List<OrderItemRespVO> getListByMonth(Integer orgType, Long tenantId, String billMonth, Integer inStatus,Integer payStatus);
 
-    OrderItemTotalRespVO getTotalByMonth(Integer orgType, Long tenantId, String billingMonth,Integer payStatus);
+    default OrderItemTotalRespVO getTotalByMonth(Integer orgType, Long tenantId, String billingMonth, Integer payStatus) {
+        return getTotalByMonth(orgType, tenantId, billingMonth, payStatus, null);
+    }
+
+    /**
+     * 账单金额汇总
+     *
+     * @param roundScale 金额小数保留位数:非空时,先对每项金额按该位数处理小数,再进行汇总;
+     *                   null 表示不对金额做小数处理(保持原样)
+     */
+    OrderItemTotalRespVO getTotalByMonth(Integer orgType, Long tenantId, String billingMonth, Integer payStatus, Integer roundScale);
 
     List<OrderItemRespVO> getReceivableListByMonth(Integer orgType, Long tenantId, String queryFLag, String billingMonth,Integer payStatus);
 
-    OrderItemTotalRespVO getOrderReceivableTotal(Integer orgType, Long tenantId, String billingMonth,Integer payStatus);
+    default OrderItemTotalRespVO getOrderReceivableTotal(Integer orgType, Long tenantId, String billingMonth, Integer payStatus) {
+        return getOrderReceivableTotal(orgType, tenantId, billingMonth, payStatus, null);
+    }
+
+    /**
+     * 应收账单金额汇总
+     *
+     * @param roundScale 金额小数保留位数:非空时,先对每项金额按该位数处理小数,再进行汇总;
+     *                   null 表示不对金额做小数处理(保持原样)
+     */
+    OrderItemTotalRespVO getOrderReceivableTotal(Integer orgType, Long tenantId, String billingMonth, Integer payStatus, Integer roundScale);
 
     /**
      * 报表用:获取当月账单明细并完成离住/退住/在住判定及备注处理

+ 10 - 1
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/order/VO/OrderItemRespVO.java

@@ -5,6 +5,7 @@ import lombok.Data;
 
 import java.math.BigDecimal;
 import java.time.LocalDate;
+import java.util.Map;
 
 /**
  * @Author: jinyd
@@ -38,7 +39,7 @@ public class OrderItemRespVO {
     private String bedName;
 
     // 房间类型
-    private String itemName;
+    private String bedItemName;
 
     // 护理等级
     private String nurseLevelName;
@@ -116,6 +117,14 @@ public class OrderItemRespVO {
     // 水电费
     private BigDecimal orderWaterElectricityAmount;
 
+    private BigDecimal orderOtherIncomeAmount;
+
+    private Map<String, BigDecimal> orderNursingItemStats;
+
+    private Map<String, BigDecimal> orderServiceItemStats;
+
+    private Map<String, BigDecimal> orderOtherIncomeItemStats;
+
     // 增值服务费
     private BigDecimal extraServiceAmount;
 

+ 18 - 0
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/sys/BusinessParamsConfigApi.java

@@ -0,0 +1,18 @@
+package cn.iocoder.yudao.module.system.api.sys;
+
+/**
+ * 系统业务参数配置 API
+ */
+public interface BusinessParamsConfigApi {
+
+    /**
+     * 获取指定机构(租户)下,某标题、某配置名称的参数值
+     *
+     * @param tenantId   租户(机构)编号
+     * @param title      标题,例如:财务模块配置
+     * @param configName 配置名称,例如:账单金额单项取整设置
+     * @return 参数值,不存在时返回 null
+     */
+    String getConfigValue(Long tenantId, String title, String configName);
+
+}

+ 69 - 0
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java

@@ -277,6 +277,75 @@ public interface ErrorCodeConstants {
     ErrorCode ELDERLY_RENEW_ORDER_NOT_EXISTS = new ErrorCode(1_008_004_000, "长者不存在续签月账单,请先生成!");
     // ========== 外出相关 1-008-005-000 ==========
     ErrorCode ELDERLY_LEAVE_UPDATE_DATE_OVER_EXPIRE = new ErrorCode(1_008_005_000, "返院时间大于合同到期时间,请检查!");
+    ErrorCode ELDERLY_LEAVE_SCAN_OUT_NOT_FOUND = new ErrorCode(1_008_005_001, "未找到已审批的外出记录,无法进行出院扫码!");
+    ErrorCode ELDERLY_LEAVE_SCAN_RETURN_NOT_FOUND = new ErrorCode(1_008_005_002, "未找到实际返回时间为空的外出记录,无法进行回院扫码!");
     // ========== 菜品相关 1-008-006-000 ==========
     ErrorCode DISHES_EXISTS = new ErrorCode(1_008_006_000, "该菜品已存在,请检查!");
+    // ========== 风险知情书 1-008-007-000 ==========
+    ErrorCode ELDERLY_EXTRA_RISK_DISCLOSURE_NOT_EXISTS = new ErrorCode(1_008_007_000, "风险知情书不存在");
+    // ========== 入住登记表 1-008-008-000 ==========
+    ErrorCode ELDERLY_EXTRA_CHECK_IN_TAB_NOT_EXISTS = new ErrorCode(1_008_008_000, "入住登记表不存在");
+    // ========== 长者安全承诺书 1-008-009-000 ==========
+    ErrorCode ELDERLY_EXTRA_SAFETY_COMMITMENT_NOT_EXISTS = new ErrorCode(1_008_009_000, "长者安全承诺书不存在");
+    // ========== 外出情况确认书 1-008-010-000 ==========
+    ErrorCode ELDERLY_EXTRA_OUTING_CONFIRMATION_FORM_NOT_EXISTS = new ErrorCode(1_008_010_000, "外出情况确认书不存在");
+    // ========== 签名代理申请 1-008-011-000 ==========
+    ErrorCode ELDERLY_EXTRA_SIGNATURE_PROXY_APPLICATION_NOT_EXISTS = new ErrorCode(1_008_011_000, "签名代理申请不存在");
+    // ========== 授权委托书 1-008-012-000 ==========
+    ErrorCode ELDERLY_EXTRA_POWER_OF_ATTORNEY_NOT_EXISTS = new ErrorCode(1_008_012_000, "授权委托书不存在");
+    // ========== 委托代理人确认表 1-008-013-000 ==========
+    ErrorCode ELDERLY_EXTRA_AUTHORIZED_AGENT_CONFIRMATION_NOT_EXISTS = new ErrorCode(1_008_013_000, "委托代理人确认表不存在");
+    // ========== 机构服务范围及收费标准 1-008-014-000 ==========
+    ErrorCode ELDERLY_EXTRA_ORG_SERVICE_SCOPE_FEE_NOT_EXISTS = new ErrorCode(1_008_014_000, "机构服务范围及收费标准不存在");
+    // ========== 房间设施设备清单 1-008-015-000 ==========
+    ErrorCode ELDERLY_EXTRA_ROOM_FACILITY_LIST_NOT_EXISTS = new ErrorCode(1_008_015_000, "房间设施设备清单不存在");
+    // ========== 体检报告项目说明 1-008-016-000 ==========
+    ErrorCode ELDERLY_EXTRA_PHYSICAL_EXAM_REPORT_NOT_EXISTS = new ErrorCode(1_008_016_000, "体检报告项目说明不存在");
+    // ========== 乙方有效证件 1-008-017-000 ==========
+    ErrorCode ELDERLY_EXTRA_PARTY_B_CERTIFICATE_NOT_EXISTS = new ErrorCode(1_008_017_000, "乙方有效证件不存在");
+    // ========== 乙方监护人(丙方)证件 1-008-018-000 ==========
+    ErrorCode ELDERLY_EXTRA_PARTY_B_GUARDIAN_CERTIFICATE_NOT_EXISTS = new ErrorCode(1_008_018_000, "乙方监护人(丙方)证件不存在");
+    // ========== 甲方登记证书及民政部门备案回执 1-008-019-000 ==========
+    ErrorCode ELDERLY_EXTRA_PARTY_A_CERTIFICATE_NOT_EXISTS = new ErrorCode(1_008_019_000, "甲方登记证书及民政部门备案回执不存在");
+    // ========== 老年人能力综合评估结果 1-008-020-000 ==========
+    ErrorCode ELDERLY_EXTRA_ELDERLY_ABILITY_EVALUATION_NOT_EXISTS = new ErrorCode(1_008_020_000, "老年人能力综合评估结果不存在");
+    // ========== 首次服务项目确认表 1-008-021-000 ==========
+    ErrorCode ELDERLY_EXTRA_FIRST_SERVICE_CONFIRMATION_NOT_EXISTS = new ErrorCode(1_008_021_000, "首次服务项目确认表不存在");
+    // ========== 变更事项确认表 1-008-022-000 ==========
+    ErrorCode ELDERLY_EXTRA_CHANGE_CONFIRMATION_NOT_EXISTS = new ErrorCode(1_008_022_000, "变更事项确认表不存在");
+    // ========== 楼层报餐 1-008-023-000 ==========
+    ErrorCode ELDERLY_BUILD_FLOOR_ORDER_FOOD_NOT_EXISTS = new ErrorCode(1_008_023_000, "楼层报餐不存在");
+
+    // ========== 预约名额预占 1-002-040-000 ==========
+    ErrorCode APPOINTMENT_CONFIG_NOT_FOUND = new ErrorCode(1_002_040_000, "预约配置不存在");
+    ErrorCode APPOINTMENT_DISABLED = new ErrorCode(1_002_040_001, "预约未开启");
+    ErrorCode APPOINTMENT_DATE_NOT_IN_RANGE = new ErrorCode(1_002_040_002, "不在可预约时间范围内");
+    ErrorCode APPOINTMENT_DATE_NOT_ALLOWED = new ErrorCode(1_002_040_003, "该日期不可预约");
+    ErrorCode APPOINTMENT_TOTAL_LIMIT = new ErrorCode(1_002_040_004, "预约名额已满");
+    ErrorCode APPOINTMENT_PERSON_LIMIT = new ErrorCode(1_002_040_005, "您预约次数已达上限");
+    ErrorCode APPOINTMENT_GROUP_LIMIT = new ErrorCode(1_002_040_006, "该时段名额已满");
+    ErrorCode APPOINTMENT_TIME_NOT_ALLOWED = new ErrorCode(1_002_040_007, "预约时间段不在可预约范围内");
+
+    // ========== 探访预约(家属小程序) 1-002-041-000 ==========
+    ErrorCode VISIT_APPOINTMENT_NOT_FOUND = new ErrorCode(1_002_041_000, "探访预约记录不存在");
+    ErrorCode VISIT_APPOINTMENT_CANCELED = new ErrorCode(1_002_041_001, "该预约已取消或已删除,无法重复取消");
+
+    // ========== 建行支付相关 1-008-025-000 ==========
+    ErrorCode EXPENSE_ORDER_PAY_AMOUNT_INVALID = new ErrorCode(1_008_025_000, "账单金额异常,无法发起支付,请联系院方核对账单!");
+
+    ErrorCode EXPENSE_ORDER_PAY_AMOUNT_NOT_MATCH = new ErrorCode(1_008_025_001, "账单金额已发生变化,请刷新后重新支付!");
+
+    ErrorCode CCB_MERCHANT_NOT_FOUND = new ErrorCode(1_008_025_002, "未配置该院区的建行商户信息,无法发起支付!");
+
+    ErrorCode CCB_ORDER_ALREADY_PAID = new ErrorCode(1_008_025_003, "该订单在建行已支付成功,请稍后刷新账单或联系院方核对!");
+
+    // ========== 医嘱模块(长期/临时/执行) 1-008-024-000 ==========
+    ErrorCode ELDERLY_DOCTOR_ORDER_NOT_EXISTS = new ErrorCode(1_008_024_000, "医嘱单不存在");
+    ErrorCode ELDERLY_DOCTOR_ORDER_ENTRY_NOT_EXISTS = new ErrorCode(1_008_024_001, "医嘱条目不存在");
+    ErrorCode ELDERLY_DOCTOR_ORDER_EXEC_NOT_EXISTS = new ErrorCode(1_008_024_002, "护士执行记录不存在");
+    ErrorCode ELDERLY_DOCTOR_ORDER_ELDER_NOT_EXISTS = new ErrorCode(1_008_024_003, "长者信息不存在");
+
+    // ========== 出院小结 1-008-026-000 ==========
+    ErrorCode ELDERLY_DISCHARGE_ABSTRACT_NOT_EXISTS = new ErrorCode(1_008_026_000, "出院小结不存在");
+    ErrorCode ELDERLY_DISCHARGE_USE_MEDICINE_NOT_EXISTS = new ErrorCode(1_008_026_001, "出院小结用药不存在");
 }

+ 2 - 1
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/change/BusinessTypeEnum.java

@@ -24,7 +24,8 @@ public enum BusinessTypeEnum implements ArrayValuable<Integer> {
     CATERING_CHANGE(6, "catering_change"), //餐饮变更
     PRICE_CHANGE(7, "price_change"), //价格变更
     TRANSFER_CHANGE(8, "transfer_change"), //转院变更
-    CHECK_IN(9, "check_in");
+    CHECK_IN(9, "check_in"),
+    CHECK_IN_WAIT(10, "check_in_wait"); //轮候入住
 
     public static final Integer[] ARRAYS = Arrays.stream(values()).map(BusinessTypeEnum::getValue).toArray(Integer[]::new);
 

+ 2 - 0
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/change/ChangeConstants.java

@@ -18,6 +18,8 @@ public interface ChangeConstants {
 
     String CATERING_CHANGE_DAILY_PAY_REMARK =  "餐饮价格变更:合同价格:%s 元/月,原价格:%s 元/月,新价格:%s元/月,原价格使用天数:%s 天,新价格使用天数:%s 天";
 
+    String BED_SWAP_DAILY_PAY_REMARK =  "床位对调费用变更:合同价格:%s 元/月,原价格:%s 元/月,新价格:%s元/月,原价格使用天数:%s 天,新价格使用天数:%s 天";
+
     String PRICE_CHANGE_DAILY_PAY_REMARK =  "价格变更:合同价格:%s 元/月,原价格:%s 元/月,新价格:%s元/月,原价格使用天数:%s 天,新价格使用天数:%s 天";
 
 //    String PRICE_CHANGE_DAILY_REFUND_REMARK =  "%s日%s类型发生价格变更,变更前月费用为%s元;变更后月费用为%s元,%s至%s日产生金额为%s元,变更后%s至%s日应付%s元";

+ 25 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/bpm/BpmAskLeaveApiImpl.java

@@ -0,0 +1,25 @@
+package cn.iocoder.yudao.module.system.api.bpm;
+
+import cn.iocoder.yudao.module.system.dal.dataobject.biz.ElderlyAskLeaveDO;
+import cn.iocoder.yudao.module.system.dal.mysql.biz.ElderlyAskLeaveMapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import org.springframework.stereotype.Service;
+import org.springframework.validation.annotation.Validated;
+
+import javax.annotation.Resource;
+
+@Service
+@Validated
+public class BpmAskLeaveApiImpl implements BpmAskLeaveApi {
+
+    @Resource
+    private ElderlyAskLeaveMapper elderlyAskLeaveMapper;
+
+    @Override
+    public void updateLeaveStatus(Long askLeaveId, Integer leaveStatus) {
+        elderlyAskLeaveMapper.update(null, new LambdaUpdateWrapper<ElderlyAskLeaveDO>()
+                .eq(ElderlyAskLeaveDO::getId, askLeaveId)
+                .set(ElderlyAskLeaveDO::getLeaveStatus, leaveStatus));
+    }
+}
+

+ 120 - 65
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/bpm/BpmCheckInApiImpl.java

@@ -159,7 +159,7 @@ public class BpmCheckInApiImpl implements BpmCheckInApi {
         insertBO.setCheckInTime(elderlyContractDO.getBeginTime());
         insertBO.setCreatedTime(new Date());
         insertBO.setElderId(createReqVO.getElderlyId());
-        insertBO.setStatus(2);
+        insertBO.setStatus(1);
         insertBO.setAssociateId(checkInApplyDO.getId());
         insertBO.setCurrentFlag(0);
         insertBO.setTenantId(createReqVO.getTenantId());
@@ -215,6 +215,11 @@ public class BpmCheckInApiImpl implements BpmCheckInApi {
         Long expenseOrderId = expenseService.createExpense(expenseSaveReqVO);
 
         ElderlyInfoDO elderlyInfoDO = elderlyInfoMapper.selectById(checkInApplyDO.getElderlyId());
+        if (elderlyInfoDO != null && expenseSaveReqVO.getInStatusType() != null) {
+            elderlyInfoDO.setInStatusType(expenseSaveReqVO.getInStatusType());
+            elderlyInfoMapper.updateById(elderlyInfoDO);
+        }
+        applyCheckInStatusOnApproval(elderlyCheckInRecordDO, checkInApplyDO, elderlyInfoDO, expenseSaveReqVO.getTenantId());
 
         notifyMessageSendApi.updateMessageByCodeAndPkMessage(BpmMessageEnum.CHECK_IN_HANDLE.getTemplateCode(), elderlyInfoDO.getId());
 
@@ -300,6 +305,55 @@ public class BpmCheckInApiImpl implements BpmCheckInApi {
         return 0;
     }
 
+    @Override
+    @TenantIgnore
+    public Integer unbindBedOnCancel(Long businessId) {
+        ElderlyCheckInRecordDO checkInRecord = recordMapper.selectById(businessId);
+        if (checkInRecord == null || checkInRecord.getAssociateId() == null) {
+            return 0;
+        }
+        CheckInApplyDO checkInApplyDO = checkInApplyMapper.selectById(checkInRecord.getAssociateId());
+        if (checkInApplyDO == null || checkInApplyDO.getBedId() == null) {
+            return 0;
+        }
+        unlockBed(checkInApplyDO.getBedId(), checkInRecord.getTenantId());
+        return 0;
+    }
+
+    private void applyCheckInStatusOnApproval(ElderlyCheckInRecordDO checkInRecordDO, CheckInApplyDO checkInApplyDO,
+                                              ElderlyInfoDO elderlyInfoDO, Long tenantId) {
+        if (checkInRecordDO == null || checkInApplyDO == null || elderlyInfoDO == null) {
+            return;
+        }
+        if (checkInApplyDO.getBedId() != null) {
+            BuildBedDO bedDO = buildBedMapper.selectById(checkInApplyDO.getBedId());
+            if (bedDO != null) {
+                elderlyInfoDO.setBedId(bedDO.getId());
+                elderlyInfoDO.setBedName(bedDO.getBedName());
+                elderlyInfoDO.setRoomId(bedDO.getRoomId());
+                elderlyInfoDO.setFloorId(bedDO.getFloorId());
+                elderlyInfoDO.setBuildId(bedDO.getBuildId());
+                lockBed(bedDO.getId(), tenantId);
+            }
+        }
+        elderlyInfoDO.setInStatus(1);
+        elderlyInfoDO.setCheckInTime(checkInApplyDO.getCheckInTime());
+        elderlyInfoDO.setNurseLevelId(checkInApplyDO.getNurseLevelId());
+        elderlyInfoDO.setIsPrivateRoom(checkInApplyDO.getIsPrivateRoom());
+        elderlyInfoDO.setSpecialCareNotes(checkInApplyDO.getSpecialCareNotes());
+        if (checkInApplyDO.getNurseLevelId() != null) {
+            NurseLevelDO nurseLevelDO = nurseLevelMapper.selectById(checkInApplyDO.getNurseLevelId());
+            elderlyInfoDO.setNurseLevelName(nurseLevelDO == null ? null : nurseLevelDO.getNurseLevelName());
+        }
+        elderlyInfoMapper.updateById(elderlyInfoDO);
+
+        checkInRecordDO.setStatus(2);
+        if (checkInApplyDO.getCheckInTime() != null) {
+            checkInRecordDO.setCheckInTime(checkInApplyDO.getCheckInTime());
+        }
+        recordMapper.updateById(checkInRecordDO);
+    }
+
     @Override
     public PageResult<CheckInRecordRespVO> getProcessInstanceCheckInPage(CheckInApiPageReqVO pageReqVO) {
         Page<CheckInRecordRespVO> page = new Page<>(pageReqVO.getPageNo(), pageReqVO.getPageSize());
@@ -320,75 +374,76 @@ public class BpmCheckInApiImpl implements BpmCheckInApi {
     @TenantIgnore
     public Integer bindBedByType(CheckInCreateReqVO createReqVO, Integer queryType) {
         CheckInExpenseSaveReqVO expenseBO = createReqVO.getExpenseBO();
-
-        // 绑定床位信息
-        ElderlyInfoDO elderlyInfo = elderlyInfoMapper.selectById(expenseBO.getElderId());
-        Long bedId = elderlyInfo.getBedId();
-        BuildBedDO bedDO = buildBedMapper.selectById(expenseBO.getBedId());
-        // queryType 0:绑定 1:解绑
-        if (0 == queryType) {
-            if (bedDO != null) {
-                elderlyInfo.setRoomId(bedDO.getRoomId());
-                elderlyInfo.setFloorId(bedDO.getFloorId());
-                elderlyInfo.setBuildId(bedDO.getBuildId());
-
-                bedDO.setStatus(1);
-                buildBedMapper.updateById(bedDO);
-
-                BuildRoomDO roomDO = buildRoomMapper.selectById(bedDO.getRoomId());
-                // TODO 为什么不在楼栋房间表增加床位数量字段,状态增加满员,更加精确形容房间的状态
-                roomDO.setStatus(1);
-                buildRoomMapper.updateById(roomDO);
-            }
-        } else {
-            // 解绑之前的床位
-            BuildBedDO historyBedDO  = buildBedMapper.selectById(bedId);
-            if (null != historyBedDO) {
-                // 释放之前的床位
-                historyBedDO.setStatus(0);
-                buildBedMapper.updateById(historyBedDO);
-                BuildRoomDO historyRoomDO = buildRoomMapper.selectById(historyBedDO.getRoomId());
-                List<BuildBedDO> buildBedDOS = buildBedMapper.selectList(new LambdaQueryWrapperX<BuildBedDO>()
-                        .eq(BuildBedDO::getRoomId, bedDO.getRoomId())
-                        .eq(BuildBedDO::getTenantId, createReqVO.getTenantId()));
-                if(null != buildBedDOS && buildBedDOS.size() == 1) {
-                    historyRoomDO.setStatus(0);
-                    buildRoomMapper.updateById(historyRoomDO);
-                }
-            }
-            if (null != bedDO) {
-                elderlyInfo.setRoomId(bedDO.getRoomId());
-                elderlyInfo.setFloorId(bedDO.getFloorId());
-                elderlyInfo.setBuildId(bedDO.getBuildId());
-
-                bedDO.setStatus(1);
-                buildBedMapper.updateById(bedDO);
-                BuildRoomDO roomDO = buildRoomMapper.selectById(bedDO.getRoomId());
-                roomDO.setStatus(1);
-                buildRoomMapper.updateById(roomDO);
-
+        if (expenseBO == null || expenseBO.getElderId() == null || expenseBO.getBedId() == null) {
+            return 0;
+        }
+        Long tenantId = createReqVO.getTenantId();
+        Long elderId = expenseBO.getElderId();
+        Long newBedId = expenseBO.getBedId();
+
+        if (Integer.valueOf(1).equals(queryType)) {
+            Long historyBedId = getLatestProcessBedId(elderId, tenantId, newBedId);
+            if (historyBedId != null) {
+                unlockBed(historyBedId, tenantId);
             }
-
         }
-        elderlyInfo.setInStatus(1);
-        elderlyInfo.setBedId(expenseBO.getBedId());
-        elderlyInfo.setCheckInTime(expenseBO.getCheckInTime());
-        elderlyInfo.setNurseLevelId(expenseBO.getNurseLevelId());
-        elderlyInfo.setIsPrivateRoom(expenseBO.getIsPrivateRoom());
-        NurseLevelDO nurseLevelDO = nurseLevelMapper.selectById(expenseBO.getNurseLevelId());
-        elderlyInfo.setNurseLevelName(nurseLevelDO == null ? null : nurseLevelDO.getNurseLevelName());
-        elderlyInfoMapper.updateById(elderlyInfo);
-        ElderlyCheckInRecordDO elderlyCheckInRecord = recordMapper.selectOne(new LambdaQueryWrapperX<ElderlyCheckInRecordDO>()
-                .eq(ElderlyCheckInRecordDO::getElderId, elderlyInfo.getId())
-                .eq(ElderlyCheckInRecordDO::getTenantId, createReqVO.getTenantId())
+        lockBed(newBedId, tenantId);
+        return 0;
+    }
+
+    private Long getLatestProcessBedId(Long elderId, Long tenantId, Long excludeBedId) {
+        ElderlyCheckInRecordDO historyRecord = recordMapper.selectOne(new LambdaQueryWrapperX<ElderlyCheckInRecordDO>()
+                .eq(ElderlyCheckInRecordDO::getElderId, elderId)
+                .eqIfPresent(ElderlyCheckInRecordDO::getTenantId, tenantId)
+                .eq(ElderlyCheckInRecordDO::getCurrentFlag, 1)
                 .orderByDesc(ElderlyCheckInRecordDO::getCreatedTime)
                 .last("LIMIT 1"));
-        if (elderlyCheckInRecord != null) {
-            elderlyCheckInRecord.setStatus(2); //修改状态为已入住
-            elderlyCheckInRecord.setCheckInTime(expenseBO.getCheckInTime());
-            recordMapper.updateById(elderlyCheckInRecord);
+        if (historyRecord == null || historyRecord.getAssociateId() == null) {
+            return null;
+        }
+        CheckInApplyDO historyApply = checkInApplyMapper.selectById(historyRecord.getAssociateId());
+        if (historyApply == null || historyApply.getBedId() == null) {
+            return null;
         }
+        if (excludeBedId != null && excludeBedId.equals(historyApply.getBedId())) {
+            return null;
+        }
+        return historyApply.getBedId();
+    }
+
+    private void unlockBed(Long bedId, Long tenantId) {
+        BuildBedDO bedDO = buildBedMapper.selectById(bedId);
+        if (bedDO == null) {
+            return;
+        }
+        bedDO.setStatus(0);
+        buildBedMapper.updateById(bedDO);
+        updateRoomStatus(bedDO.getRoomId(), tenantId);
+    }
 
-        return null;
+    private void lockBed(Long bedId, Long tenantId) {
+        BuildBedDO bedDO = buildBedMapper.selectById(bedId);
+        if (bedDO == null) {
+            return;
+        }
+        bedDO.setStatus(1);
+        buildBedMapper.updateById(bedDO);
+        updateRoomStatus(bedDO.getRoomId(), tenantId);
+    }
+
+    private void updateRoomStatus(Long roomId, Long tenantId) {
+        if (roomId == null) {
+            return;
+        }
+        BuildRoomDO roomDO = buildRoomMapper.selectById(roomId);
+        if (roomDO == null) {
+            return;
+        }
+        Long occupiedCount = buildBedMapper.selectCount(new LambdaQueryWrapperX<BuildBedDO>()
+                .eq(BuildBedDO::getRoomId, roomId)
+                .eqIfPresent(BuildBedDO::getTenantId, tenantId)
+                .ne(BuildBedDO::getStatus, 0));
+        roomDO.setStatus(occupiedCount != null && occupiedCount > 0 ? 1 : 0);
+        buildRoomMapper.updateById(roomDO);
     }
 }

+ 177 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/bpm/BpmCheckInWaitApiImpl.java

@@ -0,0 +1,177 @@
+package cn.iocoder.yudao.module.system.api.bpm;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.module.system.api.bpm.vo.CheckInWaitApiPageReqVO;
+import cn.iocoder.yudao.module.system.api.bpm.vo.CheckInWaitCreateReqVO;
+import cn.iocoder.yudao.module.system.api.bpm.vo.CheckInWaitRecordRespVO;
+import cn.iocoder.yudao.module.system.api.bpm.vo.CheckInWaitSupplementReqVO;
+import cn.iocoder.yudao.module.system.dal.dataobject.biz.*;
+import cn.iocoder.yudao.module.system.dal.mysql.biz.BuildBedMapper;
+import cn.iocoder.yudao.module.system.dal.mysql.biz.BuildFloorMapper;
+import cn.iocoder.yudao.module.system.dal.mysql.biz.BuildMapper;
+import cn.iocoder.yudao.module.system.dal.mysql.biz.BuildRoomMapper;
+import cn.iocoder.yudao.module.system.dal.mysql.biz.ElderlyCheckInWaitMapper;
+import cn.iocoder.yudao.module.system.dal.mysql.biz.NurseLevelMapper;
+import cn.iocoder.yudao.module.system.enums.change.BusinessTypeEnum;
+import cn.iocoder.yudao.module.system.service.biz.BuildService;
+import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.validation.annotation.Validated;
+
+import javax.annotation.Resource;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@Service
+@Validated
+public class BpmCheckInWaitApiImpl implements BpmCheckInWaitApi {
+
+    @Resource
+    private ElderlyCheckInWaitMapper checkInWaitMapper;
+
+    @Resource
+    private NurseLevelMapper nurseLevelMapper;
+
+    @Autowired
+    private BuildService buildService;
+
+
+    @Override
+    public Long createCheckInWait(CheckInWaitCreateReqVO reqVO) {
+        ElderlyCheckInWaitDO data = new ElderlyCheckInWaitDO();
+        data.setElderName(reqVO.getElderName());
+        data.setIdCard(reqVO.getIdCard());
+        data.setRecentLivingSituation(reqVO.getRecentLivingSituation());
+        data.setWalkSituationVideo(reqVO.getWalkSituationVideo());
+        data.setMedicalRecordsWithinSixMonth(reqVO.getMedicalRecordsWithinSixMonth());
+        data.setIdCardPhoto(reqVO.getIdCardPhoto());
+        data.setRecentDischargeSummary(reqVO.getRecentDischargeSummary());
+        data.setTenantId(reqVO.getTenantId());
+        data.setStatus(reqVO.getStatus());
+        data.setNurseLevelId(reqVO.getNurseLevelId());
+        data.setRemark(reqVO.getRemark());
+        data.setPhysicalExaminationReport(reqVO.getPhysicalExaminationReport());
+        data.setBedId(reqVO.getBedId());
+        data.setCommonMedicine(reqVO.getCommonMedicine());
+        checkInWaitMapper.insert(data);
+        return data.getId();
+    }
+
+    @Override
+    public Integer updateInfo(CheckInWaitSupplementReqVO reqVO) {
+        ElderlyCheckInWaitDO updateDO = new ElderlyCheckInWaitDO();
+        BeanUtils.copyProperties(reqVO,updateDO);
+        return checkInWaitMapper.updateById(updateDO);
+    }
+
+    @Override
+    public Integer updateFirstReviewInfo(CheckInWaitSupplementReqVO reqVO) {
+        ElderlyCheckInWaitDO updateDO = new ElderlyCheckInWaitDO();
+        updateDO.setId(reqVO.getId());
+        updateDO.setIsPsychosis(reqVO.getIsPsychosis());
+        return checkInWaitMapper.updateById(updateDO);
+    }
+    @Override
+    public Integer updateSecondReviewInfo(CheckInWaitSupplementReqVO reqVO) {
+        ElderlyCheckInWaitDO updateDO = new ElderlyCheckInWaitDO();
+        updateDO.setId(reqVO.getId());
+        updateDO.setFirstAuditName(reqVO.getFirstAuditName());
+        updateDO.setMedicalCareRemark(reqVO.getMedicalCareRemark());
+        return checkInWaitMapper.updateById(updateDO);
+    }
+
+    @Override
+    public Integer updateCheckInWaitStatus(Long id, Integer status) {
+        ElderlyCheckInWaitDO updateDO = new ElderlyCheckInWaitDO();
+        updateDO.setId(id);
+        updateDO.setStatus(status);
+        return checkInWaitMapper.updateById(updateDO);
+    }
+
+    @Override
+    public Integer updateCheckInWaitStatusByBusinessId(Long businessId, Integer status) {
+        ElderlyCheckInWaitDO updateDO = new ElderlyCheckInWaitDO();
+        updateDO.setId(businessId);
+        updateDO.setStatus(status);
+        if(status == 11){
+            updateDO.setIsPsychosis("是");
+        }
+        return checkInWaitMapper.updateById(updateDO);
+    }
+
+    @Override
+    public Integer updateThirdReviewInfo(CheckInWaitSupplementReqVO reqVO) {
+        ElderlyCheckInWaitDO updateDO = new ElderlyCheckInWaitDO();
+        updateDO.setId(reqVO.getId());
+        updateDO.setNurseLevelId(reqVO.getNurseLevelId());
+        updateDO.setRemark(reqVO.getRemark());
+        return checkInWaitMapper.updateById(updateDO);
+    }
+
+    @Override
+    public Integer updateFourthReviewInfo(CheckInWaitSupplementReqVO reqVO) {
+        ElderlyCheckInWaitDO updateDO = new ElderlyCheckInWaitDO();
+        updateDO.setId(reqVO.getId());
+        updateDO.setRemark(reqVO.getRemark());
+        updateDO.setPhysicalExaminationReport(reqVO.getPhysicalExaminationReport());
+        updateDO.setSecondLiverTwoHalfAndHalf(reqVO.getSecondLiverTwoHalfAndHalf());
+        updateDO.setSyphilis(reqVO.getSyphilis());
+        updateDO.setHiv(reqVO.getHiv());
+        updateDO.setTuberculosisBacillusAntibody(reqVO.getTuberculosisBacillusAntibody());
+        updateDO.setHepatitisC(reqVO.getHepatitisC());
+        updateDO.setHepatitisE(reqVO.getHepatitisE());
+        updateDO.setBloodRoutineExamination(reqVO.getBloodRoutineExamination());
+        updateDO.setUrineRoutines(reqVO.getUrineRoutines());
+        updateDO.setFecalRoutine(reqVO.getFecalRoutine());
+        updateDO.setLiverFunction(reqVO.getLiverFunction());
+        updateDO.setBloodBiochemistry(reqVO.getBloodBiochemistry());
+        updateDO.setRabat(reqVO.getRabat());
+        updateDO.setHeadCt(reqVO.getHeadCt());
+        updateDO.setEcg(reqVO.getEcg());
+        updateDO.setBmd(reqVO.getBmd());
+        updateDO.setCognitiveAssessmentReport(reqVO.getCognitiveAssessmentReport());
+        return checkInWaitMapper.updateById(updateDO);
+    }
+
+    @Override
+    public Integer updateFifthReviewInfo(CheckInWaitSupplementReqVO reqVO) {
+        ElderlyCheckInWaitDO updateDO = new ElderlyCheckInWaitDO();
+        updateDO.setId(reqVO.getId());
+        updateDO.setAuditName(reqVO.getAuditName());
+        return checkInWaitMapper.updateById(updateDO);
+    }
+
+    @Override
+    public Integer updateSixthReviewInfo(CheckInWaitSupplementReqVO reqVO) {
+        ElderlyCheckInWaitDO updateDO = new ElderlyCheckInWaitDO();
+        updateDO.setId(reqVO.getId());
+        updateDO.setBedId(reqVO.getBedId());
+        return checkInWaitMapper.updateById(updateDO);
+    }
+
+    @Override
+    public JSONObject selectById(Long id) {
+        ElderlyCheckInWaitDO elderlyCheckInWaitDO = checkInWaitMapper.selectById(id);
+        JSONObject result = (JSONObject) JSONObject.toJSON(elderlyCheckInWaitDO);
+        result.put("bedName",buildService.getFullBedNameByBedId(elderlyCheckInWaitDO.getBedId()));
+        NurseLevelDO nurseLevelDO = nurseLevelMapper.selectById(elderlyCheckInWaitDO.getNurseLevelId());
+        result.put("nurseLevelName", nurseLevelDO == null ? "" : nurseLevelDO.getNurseLevelName());
+        return result;
+    }
+
+    @Override
+    public PageResult<CheckInWaitRecordRespVO> getProcessInstanceCheckInWaitPage(CheckInWaitApiPageReqVO pageReqVO) {
+        Page<CheckInWaitRecordRespVO> page = new Page<>(pageReqVO.getPageNo(), pageReqVO.getPageSize());
+        pageReqVO.setType(BusinessTypeEnum.CHECK_IN_WAIT.getValue());
+        List<CheckInWaitRecordRespVO> list = checkInWaitMapper.selectCheckInWaitListPage(page, pageReqVO);
+        return new PageResult<>(list, page.getTotal());
+    }
+}

+ 57 - 2
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/bpm/BpmElderlyApiImpl.java

@@ -256,6 +256,7 @@ public class BpmElderlyApiImpl implements BpmElderlyApi {
     @Override
     public Long addElder(ElderlyInfoReqVO elderlyInfoReqVO) {
         ElderlyInfoDO elderlyInfo = BeanUtils.toBean(elderlyInfoReqVO, ElderlyInfoDO.class);
+        elderlyInfo.setElderName(elderlyInfo.getElderName().trim());
         elderlyInfo.setCreateTime(new Date());
         elderlyInfo.setUserSign(RandomUtil.randomString(18));
         elderlyInfo.setUpdateTime(new Date());
@@ -378,9 +379,63 @@ public class BpmElderlyApiImpl implements BpmElderlyApi {
         elderlyInfoDO.setInStatus(2);
         elderlyInfoMapper.updateById(elderlyInfoDO);
 
+        // 已退住长者:把进行中(1)和未开始(2)合同更新为已退住(4)
+        Long tenantId = elderlyRetreatRecordDO.getTenantId() != null
+                ? elderlyRetreatRecordDO.getTenantId()
+                : (originalElderlyInfoDO != null ? originalElderlyInfoDO.getTenantId() : TenantContextHolder.getTenantId());
+        int updatedContractCount = contractMapper.batchUpdateStatusToRetreatedByElderId(elderId, tenantId);
+        if (updatedContractCount > 0) {
+            log.info("[releaseBed] 退住长者 elderId={} 合同置为已退住(4) {} 条", elderId, updatedContractCount);
+        }
+
         BuildBedDO bedDO = buildBedMapper.selectById(originalElderlyInfoDO.getBedId());
-        bedDO.setStatus(0);
-        buildBedMapper.updateById(bedDO);
+        if(bedDO != null){
+            bedDO.setStatus(0);
+            buildBedMapper.updateById(bedDO);
+        }
+    }
+
+    @Override
+    @TenantIgnore
+    public void revokeApprovedRetreat(Long retreatRecordId) {
+        ElderlyRetreatRecordDO elderlyRetreatRecordDO = retreatRecordMapper.selectById(retreatRecordId);
+        if (elderlyRetreatRecordDO == null) {
+            throw new RuntimeException("找不到该退住申请记录");
+        }
+        Long elderId = elderlyRetreatRecordDO.getElderlyId();
+        ElderlyInfoDO originalElderlyInfoDO = elderlyInfoMapper.selectById(elderId);
+        if (originalElderlyInfoDO == null) {
+            throw new RuntimeException("找不到该长者信息");
+        }
+
+        BuildBedDO bedDO = originalElderlyInfoDO.getBedId() == null ? null : buildBedMapper.selectById(originalElderlyInfoDO.getBedId());
+        if (bedDO != null && !Objects.equals(bedDO.getStatus(), 0)) {
+            throw new RuntimeException("床位已被占用,无法撤销退住申请");
+        }
+
+        ElderlyInfoDO elderlyInfoDO = new ElderlyInfoDO();
+        elderlyInfoDO.setId(elderId);
+        elderlyInfoDO.setCheckOutTime(null);
+        elderlyInfoDO.setInStatus(1);
+        elderlyInfoMapper.updateById(elderlyInfoDO);
+
+        ElderlyCheckInRecordDO elderlyCheckInRecordDO = elderlyCheckInRecordMapper.selectOne(new LambdaQueryWrapperX<ElderlyCheckInRecordDO>()
+                .eq(ElderlyCheckInRecordDO::getElderId, elderId)
+                .eq(ElderlyCheckInRecordDO::getCurrentFlag, 0));
+        if (elderlyCheckInRecordDO != null && Objects.equals(elderlyCheckInRecordDO.getStatus(), 3)) {
+            elderlyCheckInRecordDO.setStatus(2);
+            elderlyCheckInRecordMapper.updateById(elderlyCheckInRecordDO);
+        }
+
+        if (bedDO != null) {
+            bedDO.setStatus(1);
+            buildBedMapper.updateById(bedDO);
+        }
+
+        ElderlyRetreatRecordDO updateRetreat = new ElderlyRetreatRecordDO();
+        updateRetreat.setId(retreatRecordId);
+        updateRetreat.setCurrentFlag(1);
+        retreatRecordMapper.updateById(updateRetreat);
     }
 
     @Override

+ 148 - 45
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/bpm/BpmElderlyChangeApiImpl.java

@@ -40,6 +40,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import jodd.util.StringUtil;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.core.parameters.P;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
@@ -127,6 +128,9 @@ public class BpmElderlyChangeApiImpl implements BpmElderlyChangeApi {
     @Resource
     private ExpenseOrderItemMapper expenseOrderItemMapper;
 
+    @Autowired
+    private NurseLevelMapper nurseLevelMapper;
+
     @Override
     @Transactional(rollbackFor = Exception.class)
     public Long addElderlyBedChangeRecord(ElderlyChangeRecordReqVO bedChangeRecordReqVO) {
@@ -251,6 +255,13 @@ public class BpmElderlyChangeApiImpl implements BpmElderlyChangeApi {
         return checkResult;
     }
 
+    @Override
+    @TenantIgnore
+    public List<ElderlyCurrentChargeItemRespVO> listCurrentChargeItemList(Long tenantId, Integer orgType, Integer identification,
+                                                                          List<Long> overheadChargeIds) {
+        return changeRecordMapper.selectCurrentChargeItemList(tenantId, orgType, identification, overheadChargeIds);
+    }
+
     @Override
     @Transactional(rollbackFor = Exception.class)
     @TenantIgnore
@@ -258,6 +269,19 @@ public class BpmElderlyChangeApiImpl implements BpmElderlyChangeApi {
         ElderlyChangeRecordDO bedChangeRecordDO = changeRecordMapper.selectById(businessId);
         // 1. 获取当前费用的价格 TODO 不应该在这里查询,前端是能获取到的,应该在插入变更记录的时候就要录入
         Long elderlyId = bedChangeRecordDO.getElderlyId();
+        // 护理变更
+        if(identification == 2){
+            List<NurseLevelDO> nurseLevelDOS = nurseLevelMapper.selectList(new LambdaQueryWrapperX<NurseLevelDO>()
+                    .eq(NurseLevelDO::getNurseLevelName, bedChangeRecordDO.getExpectName()));
+            if(CollectionUtils.isNotEmpty(nurseLevelDOS)){
+                NurseLevelDO nurseLevelDO = nurseLevelDOS.get(0);
+                ElderlyInfoDO elderlyInfoDO = new ElderlyInfoDO();
+                elderlyInfoDO.setId(elderlyId);
+                elderlyInfoDO.setNurseLevelId(nurseLevelDO.getId());
+                elderlyInfoDO.setNurseLevelName(nurseLevelDO.getNurseLevelName());
+                elderlyInfoMapper.updateById(elderlyInfoDO);
+            }
+        }
         checkChangeStartDate(elderlyId,bedChangeRecordDO.getChangeDate());
         BedChangeCategoryVO chargeCategory =  expenseMapper.getChargeCategory(elderlyId, identification);
 
@@ -270,11 +294,12 @@ public class BpmElderlyChangeApiImpl implements BpmElderlyChangeApi {
         // 增加原始金额的更新,因为原始金额是后续增加的字段,需要补充
         bedChangeRecordDO.setOriginalAmount(currentExpense);
         changeRecordMapper.updateById(bedChangeRecordDO);
+        // 滞后月
+        String hysteresisMonth = bedChangeRecordDO.getHysteresisMonth();
         // 2. 对比变更后的价格
         BigDecimal changeExpense = bedChangeRecordDO.getActualAmount();
 
-        // 3. 生成两条日常费用
-//        LocalDate now = LocalDate.now();
+        // 3. 生成日常费用
         BigDecimal daysInCurrentMonth = BigDecimal.valueOf(bedChangeRecordDO.getChangeDate().lengthOfMonth());
         LocalDate firstDayOfMonth = bedChangeRecordDO.getChangeDate().withDayOfMonth(1);
         // 原价格使用天数
@@ -282,14 +307,7 @@ public class BpmElderlyChangeApiImpl implements BpmElderlyChangeApi {
         LocalDate endDay = bizUtil.getEndDateWithExpireDate(elderlyId,null,firstDayOfMonth.with(TemporalAdjusters.lastDayOfMonth()));
         // 新价格使用天数
         BigDecimal changeDays = new BigDecimal(Math.abs(ChronoUnit.DAYS.between(endDay,bedChangeRecordDO.getChangeDate()))+1);
-
-
-
-
-
-
 //        BED_CHANGE_DAILY_PAY_REMARK
-
 //        String currentRemark = String.format(BED_CHANGE_DAILY_REFUND_REMARK, bedChangeRecordDO.getChangeDate(),
 //                chargeCategory.getItemName(), currentExpense.setScale(2),
 //                bedChangeRecordDO.getExpectName(), changeExpense.setScale(2), firstDayOfMonth,
@@ -340,15 +358,49 @@ public class BpmElderlyChangeApiImpl implements BpmElderlyChangeApi {
             // 最终价格 = 新 - 原
             finalAmount = changeFinalAmount.subtract(currentFinalAmount);
         }
-        log.info("创建人:{}", SecurityFrameworkUtils.getLoginUserNickname());
         if(finalAmount.compareTo(BigDecimal.ZERO) != 0){
+            DateTimeFormatter billingMonthFormatter = DateTimeFormatter.ofPattern("yyyy-MM");
+            String changeBillingMonth = bedChangeRecordDO.getChangeDate().format(billingMonthFormatter);
+            YearMonth changeYearMonth = YearMonth.parse(changeBillingMonth, billingMonthFormatter);
+            // 滞后年月
+            YearMonth hysteresisYearMonth = null;
+            if (StringUtils.isNotBlank(hysteresisMonth)) {
+                try {
+                    hysteresisYearMonth = YearMonth.parse(hysteresisMonth, billingMonthFormatter);
+                } catch (Exception ignored) {
+                    hysteresisYearMonth = null;
+                }
+            }
             Date currentDate = new Date();
             BigDecimal dailyExpensesDOAmount = finalAmount;
             // 查询已生成账单,对生效期内的已生成账单增加日常费用记录
             List<ExpenseOrderDO> generateExpenseOrder = expenseOrderMapper.selectList(new LambdaQueryWrapperX<ExpenseOrderDO>()
                     .eq(ExpenseOrderDO::getElderId, elderlyId)
-                    .ge(ExpenseOrderDO::getBillingMonth, bedChangeRecordDO.getChangeDate().format(DateTimeFormatter.ofPattern("yyyy-MM"))));
+                    .ge(ExpenseOrderDO::getBillingMonth, changeBillingMonth)
+                    .ne(ExpenseOrderDO::getType,4));
             if (generateExpenseOrder.size() > 0) {
+                List<String> targetBillingMonths = new ArrayList<>(generateExpenseOrder.size());
+                Set<String> targetBillingMonthSet = new HashSet<>(generateExpenseOrder.size());
+                for (ExpenseOrderDO e : generateExpenseOrder) {
+                    String targetBillingMonth = resolveAttributionBillMonth(e.getBillingMonth(), changeYearMonth, hysteresisYearMonth);
+                    targetBillingMonths.add(targetBillingMonth);
+                    targetBillingMonthSet.add(targetBillingMonth);
+                }
+
+                Map<String, ExpenseOrderDO> targetOrderMap = new HashMap<>();
+                // 滞后年月,月账单映射处理
+                if (CollectionUtil.isNotEmpty(targetBillingMonthSet)) {
+                    List<ExpenseOrderDO> targetOrders = expenseOrderMapper.selectList(new LambdaQueryWrapperX<ExpenseOrderDO>()
+                            .eq(ExpenseOrderDO::getElderId, elderlyId)
+                            .ne(ExpenseOrderDO::getType,4)
+                            .in(ExpenseOrderDO::getBillingMonth, targetBillingMonthSet)
+                            .orderByDesc(ExpenseOrderDO::getCreatedTime));
+                    for (ExpenseOrderDO targetOrder : targetOrders) {
+                        targetOrderMap.putIfAbsent(targetOrder.getBillingMonth(), targetOrder);
+                    }
+                }
+
+                YearMonth finalHysteresisYearMonth = hysteresisYearMonth;
                 List<DailyExpensesDO> currentDailyExpensesDOList= generateExpenseOrder.stream().map((e) -> {
                     DailyExpensesDO currentDailyExpensesDO = new DailyExpensesDO();
                     currentDailyExpensesDO.setElderId(elderlyId);
@@ -357,7 +409,8 @@ public class BpmElderlyChangeApiImpl implements BpmElderlyChangeApi {
                     currentDailyExpensesDO.setItemName(chargeCategory.getItemName());
                     currentDailyExpensesDO.setType(2);
                     currentDailyExpensesDO.setPrice(changeExpense.setScale(2));
-                    if(bedChangeRecordDO.getChangeDate().format(DateTimeFormatter.ofPattern("yyyy-MM")).equals(e.getBillingMonth())){
+                    currentDailyExpensesDO.setTenantId(e.getTenantId());
+                    if(changeBillingMonth.equals(e.getBillingMonth())){
                         currentDailyExpensesDO.setAmount(dailyExpensesDOAmount);
                         currentDailyExpensesDO.setStartDate(bedChangeRecordDO.getChangeDate());
                         currentDailyExpensesDO.setEndDate(endDay);
@@ -374,10 +427,13 @@ public class BpmElderlyChangeApiImpl implements BpmElderlyChangeApi {
                     currentDailyExpensesDO.setCreatedTime(currentDate);
                     currentDailyExpensesDO.setAccountingTime(currentDate);
                     currentDailyExpensesDO.setCount(1);
-                    currentDailyExpensesDO.setAttributionBillTime(e.getBillingMonth());
-                    // 未缴费并且未锁定的账单
-                    if(!(e.getPayStatus().equals(BooleanEnum.TRUE.getValue())) && !e.getIsLock()){
+                    String targetBillingMonth = resolveAttributionBillMonth(e.getBillingMonth(), changeYearMonth, finalHysteresisYearMonth);
+                    currentDailyExpensesDO.setAttributionBillTime(targetBillingMonth);
+                    ExpenseOrderDO targetExpenseOrder = targetOrderMap.get(targetBillingMonth);
+                    if(targetExpenseOrder != null && !(targetExpenseOrder.getPayStatus().equals(BooleanEnum.TRUE.getValue())) && !targetExpenseOrder.getIsLock()){
                         currentDailyExpensesDO.setIsGenerateBill(BooleanEnum.TRUE.getValue());
+                    } else {
+                        currentDailyExpensesDO.setIsGenerateBill(BooleanEnum.FALSE.getValue());
                     }
                     return currentDailyExpensesDO;
                 }).collect(Collectors.toList());
@@ -386,12 +442,11 @@ public class BpmElderlyChangeApiImpl implements BpmElderlyChangeApi {
                 // 为未缴费的账单创建账单明细项并更新账单金额
                 // 不需要做日常费用isGenerateBill字段更新,因为上面插入时已经完成了,减少了数据库的操作
                 for (int i = 0; i < generateExpenseOrder.size(); i++) {
-                    ExpenseOrderDO e = generateExpenseOrder.get(i);
                     DailyExpensesDO currentDailyExpensesDO = currentDailyExpensesDOList.get(i);
-                    // 未缴费并且未锁定的账单
-                    if(!e.getPayStatus().equals(BooleanEnum.TRUE.getValue()) && !e.getIsLock()){
+                    ExpenseOrderDO targetExpenseOrder = targetOrderMap.get(targetBillingMonths.get(i));
+                    if(targetExpenseOrder != null && !targetExpenseOrder.getPayStatus().equals(BooleanEnum.TRUE.getValue()) && !targetExpenseOrder.getIsLock()){
                         ExpenseOrderItemDO expenseOrderItemDO = new ExpenseOrderItemDO();
-                        expenseOrderItemDO.setExpenseOrderId(e.getId());
+                        expenseOrderItemDO.setExpenseOrderId(targetExpenseOrder.getId());
                         expenseOrderItemDO.setSourceExpenseItemId(currentDailyExpensesDO.getId());
                         expenseOrderItemDO.setExpenseSource(BusinessConstants.DAILY_EXPENSES);
                         expenseOrderItemDO.setItemCategoryName(currentDailyExpensesDO.getItemCategory());
@@ -407,25 +462,32 @@ public class BpmElderlyChangeApiImpl implements BpmElderlyChangeApi {
                         expenseOrderItemDO.setEndDate(currentDailyExpensesDO.getEndDate());
                         expenseOrderItemDO.setCreatedBy(SecurityFrameworkUtils.getLoginUserNickname());
                         expenseOrderItemDO.setCreatedTime(currentDate);
-                        expenseOrderItemDO.setTenantId(e.getTenantId());
+                        expenseOrderItemDO.setTenantId(targetExpenseOrder.getTenantId());
                         expenseOrderItemDO.setPayStatus(0); // 未缴费状态
                         expenseOrderItemMapper.insert(expenseOrderItemDO);
                         
                         // 更新账单金额
-                        BigDecimal currentAmount = e.getActualAmount() != null ? e.getActualAmount() : BigDecimal.ZERO;
+                        BigDecimal currentAmount = targetExpenseOrder.getActualAmount() != null ? targetExpenseOrder.getActualAmount() : BigDecimal.ZERO;
                         BigDecimal newAmount = currentAmount.add(expenseOrderItemDO.getTotalAmount());
                         ExpenseOrderDO updateExpenseOrderDO = new ExpenseOrderDO();
-                        updateExpenseOrderDO.setId(e.getId());
+                        updateExpenseOrderDO.setId(targetExpenseOrder.getId());
                         updateExpenseOrderDO.setActualAmount(newAmount);
                         expenseOrderMapper.updateById(updateExpenseOrderDO);
+                        targetExpenseOrder.setActualAmount(newAmount);
                     }
                 }
             }
             // 生效的当月查询是否有账单,无账单则生成日常费用,有账单的情况上面已生成不用重复生成
             ExpenseOrderDO expenseOrder = expenseOrderMapper.selectOne(new LambdaQueryWrapperX<ExpenseOrderDO>()
                     .eq(ExpenseOrderDO::getElderId, elderlyId)
-                    .eq(ExpenseOrderDO::getBillingMonth, bedChangeRecordDO.getChangeDate().format(DateTimeFormatter.ofPattern("yyyy-MM"))));
+                    .eq(ExpenseOrderDO::getBillingMonth, changeBillingMonth)
+                    .ne(ExpenseOrderDO::getType,4));
             if(expenseOrder == null){
+                String targetBillingMonth = resolveAttributionBillMonth(changeBillingMonth, changeYearMonth, hysteresisYearMonth);
+                ExpenseOrderDO targetExpenseOrder = expenseOrderMapper.selectOne(new LambdaQueryWrapperX<ExpenseOrderDO>()
+                        .eq(ExpenseOrderDO::getElderId, elderlyId)
+                        .eq(ExpenseOrderDO::getBillingMonth, targetBillingMonth)
+                        .ne(ExpenseOrderDO::getType,4));
                 DailyExpensesDO currentDailyExpensesDO = new DailyExpensesDO();
                 currentDailyExpensesDO.setElderId(elderlyId);
                 currentDailyExpensesDO.setItemId(chargeCategory.getOverheadChargeId());
@@ -441,23 +503,50 @@ public class BpmElderlyChangeApiImpl implements BpmElderlyChangeApi {
                 currentDailyExpensesDO.setEndDate(endDay);
                 currentDailyExpensesDO.setAccountingTime(currentDate);
                 currentDailyExpensesDO.setCount(1);
-                currentDailyExpensesDO.setAttributionBillTime(bedChangeRecordDO.getChangeDate().format(DateTimeFormatter.ofPattern("yyyy-MM")));
+                currentDailyExpensesDO.setAttributionBillTime(targetBillingMonth);
+                if(targetExpenseOrder != null && !targetExpenseOrder.getPayStatus().equals(BooleanEnum.TRUE.getValue()) && !targetExpenseOrder.getIsLock()){
+                    currentDailyExpensesDO.setIsGenerateBill(BooleanEnum.TRUE.getValue());
+                } else {
+                    currentDailyExpensesDO.setIsGenerateBill(BooleanEnum.FALSE.getValue());
+                }
                 dailyExpensesMapper.insert(currentDailyExpensesDO);
+                if(currentDailyExpensesDO.getIsGenerateBill().equals(BooleanEnum.TRUE.getValue())){
+                    ExpenseOrderItemDO expenseOrderItemDO = new ExpenseOrderItemDO();
+                    expenseOrderItemDO.setExpenseOrderId(targetExpenseOrder.getId());
+                    expenseOrderItemDO.setSourceExpenseItemId(currentDailyExpensesDO.getId());
+                    expenseOrderItemDO.setExpenseSource(BusinessConstants.DAILY_EXPENSES);
+                    expenseOrderItemDO.setItemCategoryName(currentDailyExpensesDO.getItemCategory());
+                    expenseOrderItemDO.setItemName(currentDailyExpensesDO.getItemName());
+                    expenseOrderItemDO.setPrice(currentDailyExpensesDO.getPrice());
+                    expenseOrderItemDO.setActualPrice(currentDailyExpensesDO.getPrice());
+                    expenseOrderItemDO.setTotalAmount(currentDailyExpensesDO.getAmount());
+                    expenseOrderItemDO.setRoundAmount(currentDailyExpensesDO.getAmount().setScale(0, RoundingMode.HALF_UP));
+                    expenseOrderItemDO.setRoundTwoDecimalAmount(currentDailyExpensesDO.getAmount().setScale(2, RoundingMode.HALF_UP));
+                    expenseOrderItemDO.setCount(currentDailyExpensesDO.getCount());
+                    expenseOrderItemDO.setType(currentDailyExpensesDO.getType());
+                    expenseOrderItemDO.setStartDate(currentDailyExpensesDO.getStartDate());
+                    expenseOrderItemDO.setEndDate(currentDailyExpensesDO.getEndDate());
+                    expenseOrderItemDO.setCreatedBy(SecurityFrameworkUtils.getLoginUserNickname());
+                    expenseOrderItemDO.setCreatedTime(currentDate);
+                    expenseOrderItemDO.setTenantId(targetExpenseOrder.getTenantId());
+                    expenseOrderItemDO.setPayStatus(0);
+                    expenseOrderItemMapper.insert(expenseOrderItemDO);
+
+                    BigDecimal currentAmount = targetExpenseOrder.getActualAmount() != null ? targetExpenseOrder.getActualAmount() : BigDecimal.ZERO;
+                    BigDecimal newAmount = currentAmount.add(expenseOrderItemDO.getTotalAmount());
+                    expenseOrderMapper.updateById(new ExpenseOrderDO().setId(targetExpenseOrder.getId()).setActualAmount(newAmount));
+                }
             }
         }
-
-        if (0 == currentExpense.setScale(2).compareTo(changeExpense.setScale(2))) {
-            return;
-        }
         // 获取最后一次变更的时间
 //        ElderlyChangeRecordDO currentLastRecordDO = changeRecordMapper.selectLastRecordByElderId(elderlyId,
 //                bpmType, changeType, bedChangeRecordDO.getChangeDate(), bedChangeRecordDO.getId());
         // 4. 更新月度账单配置
         List<ExpenseItemDO> expenseItemDOS = expenseItemMapper.selectList(new LambdaQueryWrapperX<ExpenseItemDO>()
                 .eq(ExpenseItemDO::getExpenseId, chargeCategory.getExpenseId()));
-        ExpenseDO expenseDO = new ExpenseDO();
-        expenseMapper.insert(expenseDO.setElderId(elderlyId)
-                .setCreatedTime(new Date())
+        ExpenseDO expenseDO = expenseMapper.selectById(chargeCategory.getExpenseId());
+        expenseDO.setId(null);
+        expenseMapper.insert(expenseDO.setCreatedTime(new Date())
                 .setCreatedBy(SecurityFrameworkUtils.getLoginUserNickname())
                 .setTenantId(startTenantId));
         BigDecimal totalAmout = BigDecimal.ZERO;
@@ -484,17 +573,18 @@ public class BpmElderlyChangeApiImpl implements BpmElderlyChangeApi {
                 udpateExpenseItemDO.setId(item.getId());
                 udpateExpenseItemDO.setChangeEndDate(changeEndDate);
                 expenseItemMapper.updateById(udpateExpenseItemDO);
-                item.setChangeStartDate(bedChangeRecordDO.getChangeDate());
             } else {
                 ExpenseItemDO udpateExpenseItemDO = new ExpenseItemDO();
                 udpateExpenseItemDO.setId(item.getId());
                 udpateExpenseItemDO.setChangeFlag(1);
                 if (item.getIsMonthlyExpense() == 1) {
-                    udpateExpenseItemDO.setChangeEndDate(item.getChangeStartDate().minusDays(1));
+                    udpateExpenseItemDO.setChangeEndDate(changeEndDate);
                 }
                 expenseItemMapper.updateById(udpateExpenseItemDO);
             }
             item.setId(null);
+            item.setCreateTime(new Date());
+            item.setChangeStartDate(bedChangeRecordDO.getChangeDate());
             totalAmout.add(item.getActualAmount());
         });
         if(totalAmout.compareTo(BigDecimal.ZERO) != 0) {
@@ -506,6 +596,7 @@ public class BpmElderlyChangeApiImpl implements BpmElderlyChangeApi {
         ExpenseOrderDO expenseOrderDO = expenseOrderMapper.selectOne(new LambdaQueryWrapperX<ExpenseOrderDO>()
                         .eq(ExpenseOrderDO::getElderId, elderlyId)
                         .eq(ExpenseOrderDO::getBillingMonth, bedChangeRecordDO.getChangeDate().format(DateTimeFormatter.ofPattern("yyyy-MM")))
+                        .eq(ExpenseOrderDO::getPayStatus,BooleanEnum.FALSE.getValue())
                         .eq(ExpenseOrderDO::getType, 2)
                         .eq(ExpenseOrderDO::getTenantId, startTenantId));
         if (null == expenseOrderDO) {
@@ -587,13 +678,13 @@ public class BpmElderlyChangeApiImpl implements BpmElderlyChangeApi {
         }
         List<ExpenseItemDO> expenseItemDOS = expenseItemMapper.selectList(new LambdaQueryWrapperX<ExpenseItemDO>()
                 .eq(ExpenseItemDO::getExpenseId, categoryList.get(0).getExpenseId()));
-        ExpenseDO expenseDO = new ExpenseDO();
+        ExpenseDO expenseDO = expenseMapper.selectById(categoryList.get(0).getExpenseId());
+        expenseDO.setId(null);
         expenseMapper.insert(expenseDO.setElderId(elderlyId)
                 .setTenantId(TenantContextHolder.getTenantId())
                 .setCreatedTime(new Date())
                 .setCreatedBy(SecurityFrameworkUtils.getLoginUserNickname()));
         BigDecimal totalAmout = BigDecimal.ZERO;
-        Date date = new Date();
         priceChangeRecordDOS.forEach(priceChange ->{
             categoryList.forEach(category ->{
                 if (category.getOverheadChargeId().equals(priceChange.getOriginalId())){
@@ -625,7 +716,7 @@ public class BpmElderlyChangeApiImpl implements BpmElderlyChangeApi {
 
                     BigDecimal finalAmount = null;
 
-                    if (0 == currentExpense.setScale(2).compareTo(changeExpense.setScale(2))) {
+                    if (0 == currentExpense.compareTo(changeExpense)) {
                         finalAmount = BigDecimal.ZERO;
                     } else if(daysInCurrentMonth.equals(changeDays)){
                         finalAmount = changeExpense.subtract(currentExpense);
@@ -641,12 +732,13 @@ public class BpmElderlyChangeApiImpl implements BpmElderlyChangeApi {
                         // 最终价格 = 新 - 原
                         finalAmount = changeFinalAmount.subtract(currentFinalAmount);
                     }
-                    String currentRemark = String.format(PRICE_CHANGE_DAILY_PAY_REMARK, currentExpense.setScale(2),
-                            currentExpense.setScale(2), changeExpense.setScale(2), currentDays, changeDays);
+                    String currentRemark = String.format(PRICE_CHANGE_DAILY_PAY_REMARK, currentExpense.setScale(2,RoundingMode.HALF_UP),
+                            currentExpense.setScale(2,RoundingMode.HALF_UP), changeExpense.setScale(2,RoundingMode.HALF_UP), currentDays, changeDays);
                     Date currentDate = new Date();
                     // 查询已生成账单,对生效期内的已生成账单增加日常费用记录
                     List<ExpenseOrderDO> generateExpenseOrder = expenseOrderMapper.selectList(new LambdaQueryWrapperX<ExpenseOrderDO>()
                             .eq(ExpenseOrderDO::getElderId, elderlyId)
+                            .ne(ExpenseOrderDO::getType,4)
                             .ge(ExpenseOrderDO::getBillingMonth, changeRecordDO.getChangeDate().format(DateTimeFormatter.ofPattern("yyyy-MM"))));
                     BigDecimal dailyExpensesDOAmount = finalAmount;
                     if (generateExpenseOrder.size() > 0) {
@@ -657,7 +749,8 @@ public class BpmElderlyChangeApiImpl implements BpmElderlyChangeApi {
                             currentDailyExpensesDO.setItemCategory(itemCategory);
                             currentDailyExpensesDO.setItemName(category.getItemName());
                             currentDailyExpensesDO.setType(2);
-                            currentDailyExpensesDO.setPrice(changeExpense.setScale(2));
+                            currentDailyExpensesDO.setTenantId(e.getTenantId());
+                            currentDailyExpensesDO.setPrice(changeExpense.setScale(2,RoundingMode.HALF_UP));
                             if(changeRecordDO.getChangeDate().format(DateTimeFormatter.ofPattern("yyyy-MM")).equals(e.getBillingMonth())){
                                 currentDailyExpensesDO.setAmount(dailyExpensesDOAmount);
                                 currentDailyExpensesDO.setStartDate(changeRecordDO.getChangeDate());
@@ -668,8 +761,8 @@ public class BpmElderlyChangeApiImpl implements BpmElderlyChangeApi {
                                 YearMonth yearMonth = YearMonth.parse(e.getBillingMonth());
                                 currentDailyExpensesDO.setStartDate(yearMonth.atDay(1));
                                 currentDailyExpensesDO.setEndDate(yearMonth.atEndOfMonth());
-                                currentDailyExpensesDO.setRemarks(String.format(PRICE_CHANGE_DAILY_PAY_REMARK, currentExpense.setScale(2), currentExpense.setScale(2),
-                                        changeExpense.setScale(2), 0, yearMonth.lengthOfMonth()));
+                                currentDailyExpensesDO.setRemarks(String.format(PRICE_CHANGE_DAILY_PAY_REMARK, currentExpense.setScale(2,RoundingMode.HALF_UP), currentExpense.setScale(2,RoundingMode.HALF_UP),
+                                        changeExpense.setScale(2,RoundingMode.HALF_UP), 0, yearMonth.lengthOfMonth()));
                             }
                             currentDailyExpensesDO.setCreatedBy(SecurityFrameworkUtils.getLoginUserNickname());
                             currentDailyExpensesDO.setCreatedTime(currentDate);
@@ -723,6 +816,7 @@ public class BpmElderlyChangeApiImpl implements BpmElderlyChangeApi {
                     // 生效的当月查询是否有账单,无账单则生成日常费用,有账单的情况上面已生成不用重复生成
                     ExpenseOrderDO expenseOrder = expenseOrderMapper.selectOne(new LambdaQueryWrapperX<ExpenseOrderDO>()
                             .eq(ExpenseOrderDO::getElderId, elderlyId)
+                            .ne(ExpenseOrderDO::getType,4)
                             .eq(ExpenseOrderDO::getBillingMonth, changeRecordDO.getChangeDate().format(DateTimeFormatter.ofPattern("yyyy-MM"))));
                     if(expenseOrder == null){
                         DailyExpensesDO currentDailyExpensesDO = new DailyExpensesDO();
@@ -731,7 +825,7 @@ public class BpmElderlyChangeApiImpl implements BpmElderlyChangeApi {
                         currentDailyExpensesDO.setItemCategory(itemCategory);
                         currentDailyExpensesDO.setItemName(category.getItemName());
                         currentDailyExpensesDO.setType(2);
-                        currentDailyExpensesDO.setPrice(changeExpense.setScale(2));
+                        currentDailyExpensesDO.setPrice(changeExpense.setScale(2,RoundingMode.HALF_UP));
                         currentDailyExpensesDO.setAmount(dailyExpensesDOAmount);
                         currentDailyExpensesDO.setRemarks(currentRemark);
                         currentDailyExpensesDO.setCreatedBy(SecurityFrameworkUtils.getLoginUserNickname());
@@ -760,16 +854,16 @@ public class BpmElderlyChangeApiImpl implements BpmElderlyChangeApi {
                     item.setTotalAmount(priceChange.getAmount());
                     udpateExpenseItemDO.setChangeFlag(1);
                     udpateExpenseItemDO.setChangeEndDate(changeEndDate);
-                    item.setChangeStartDate(changeRecordDO.getChangeDate());
                     expenseItemMapper.updateById(udpateExpenseItemDO);
                 }else {
                     if(item.getIsMonthlyExpense().equals(BooleanEnum.TRUE.getValue())){
-                        udpateExpenseItemDO.setChangeEndDate(item.getChangeStartDate().minusDays(1));
+                        udpateExpenseItemDO.setChangeEndDate(changeEndDate);
                         expenseItemMapper.updateById(udpateExpenseItemDO);
                     }
                 }
                 item.setId(null);
-                item.setCreateTime(date);
+                item.setChangeStartDate(changeRecordDO.getChangeDate());
+                item.setCreateTime(new Date());
                 totalAmout.add(item.getActualAmount());
             });
         });
@@ -1019,4 +1113,13 @@ public class BpmElderlyChangeApiImpl implements BpmElderlyChangeApi {
         }
         throw exception(ErrorCodeConstants.CHANGE_START_DATE_GREATER_EXPIRE_DATE);
     }
+
+    private String resolveAttributionBillMonth(String sourceBillingMonth, YearMonth changeYearMonth, YearMonth hysteresisYearMonth) {
+        if (hysteresisYearMonth == null || changeYearMonth == null || StringUtils.isBlank(sourceBillingMonth)) {
+            return sourceBillingMonth;
+        }
+        YearMonth sourceYearMonth = YearMonth.parse(sourceBillingMonth);
+        long monthOffset = ChronoUnit.MONTHS.between(changeYearMonth, sourceYearMonth);
+        return hysteresisYearMonth.plusMonths(monthOffset).toString();
+    }
 }

+ 269 - 87
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/bpm/BpmElderlyExpenseApiImpl.java

@@ -17,6 +17,7 @@ import cn.iocoder.yudao.module.system.api.wechat.dto.CorpSendMessage;
 import cn.iocoder.yudao.module.system.api.wechat.dto.CorpWechatTextcard;
 import cn.iocoder.yudao.module.system.api.wechat.dto.WechatMessageSaveReq;
 import cn.iocoder.yudao.module.system.controller.admin.biz.vo.ExpenseOrderItemVO;
+import cn.iocoder.yudao.module.system.controller.admin.biz.vo.ExpenseOrderSaveVO;
 import cn.iocoder.yudao.module.system.controller.admin.biz.vo.ExpenseOrderVO;
 import cn.iocoder.yudao.module.system.controller.admin.biz.vo.RefundSettlementOrderRespVO;
 import cn.iocoder.yudao.module.system.dal.dataobject.biz.*;
@@ -32,8 +33,10 @@ import cn.iocoder.yudao.module.system.enums.notice.NotifyPriorityEnum;
 import cn.iocoder.yudao.module.system.enums.notice.NotifyTypeEnum;
 import cn.iocoder.yudao.module.system.service.biz.ExpenseOrderService;
 import cn.iocoder.yudao.module.system.service.biz.RefundSettlementOrderService;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
 import com.mzt.logapi.starter.annotation.LogRecord;
 import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
 import org.jetbrains.annotations.NotNull;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
@@ -49,10 +52,12 @@ import java.time.YearMonth;
 import java.time.format.DateTimeFormatter;
 import java.time.temporal.TemporalAdjusters;
 import java.util.*;
+import java.util.stream.Collectors;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.APPLY_RETREAT_ASK_LEAVE_EXISTS;
 import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.APPLY_RETREAT_CURRENT_EXPENSE_ORDER_NOT_EXISTS;
+import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.APPLY_RETREAT_TEMP_OUT_EXISTS;
 import static cn.iocoder.yudao.module.system.enums.LogRecordConstants.*;
 import static cn.iocoder.yudao.module.system.enums.NoticeConstants.REFUND_SETTLEMENT_HANDLE_URL;
 import static cn.iocoder.yudao.module.system.enums.change.ChangeConstants.RETREAT_DAILY_PAY_REMARK;
@@ -80,10 +85,6 @@ public class BpmElderlyExpenseApiImpl implements BpmElderlyExpenseApi {
     @Resource
     private ExpenseOrderMapper expenseOrderMapper;
 
-
-    @Resource
-    private ExpenseOrderItemMapper expenseOrderItemMapper;
-
     @Resource
     private AdminUserMapper userMapper;
     @Resource
@@ -97,11 +98,21 @@ public class BpmElderlyExpenseApiImpl implements BpmElderlyExpenseApi {
 
     @Autowired
     private ElderlyAskLeaveMapper askLeaveMapper;
+    @Autowired
+    private ElderlyTempOutMapper elderlyTempOutMapper;
     @Resource
     private RefundSettlementOrderService refundSettlementOrderService;
     @Resource
     private RefundSettlementOrderItemMapper refundSettlementOrderItemMapper;
 
+    @Autowired
+    private ExpenseSubsidyMapper expenseSubsidyMapper;
+    @Autowired
+    private ElderlyConsumerVouchersMapper consumerVouchersMapper;
+
+    @Autowired
+    private ExpenseOrderService expenseOrderService;
+
     @Override
     @LogRecord(type = EXPENSE_SETTLEMENT_TYPE, subType = EXPENSE_SETTLEMENT_TYPE_SUB_TYPE, bizNo = "{{#elderlyInfo.id}}",
             success = EXPENSE_SETTLEMENT_INSERT_SUCCESS)
@@ -115,12 +126,30 @@ public class BpmElderlyExpenseApiImpl implements BpmElderlyExpenseApi {
                 .eq(ElderlyCheckInRecordDO::getCurrentFlag,0));
         // 费用截止日期
         LocalDate costDeadlineDate = elderlyRetreatRecord.getCostDeadlineDate();
+        // 更新费用结束时间
+        updateExpenseItemEndDate(elderlyInfo.getId(),costDeadlineDate);
         YearMonth costDeadlineYearMonth = YearMonth.from(costDeadlineDate);
-        // 将费用截止日期后的账单更新为不显示
-        expenseOrderMapper.update(new ExpenseOrderDO().setIsShow(Boolean.FALSE),new LambdaQueryWrapperX<ExpenseOrderDO>()
+        ExpenseOrderDO costDeadLineExpenseOrder = null;
+        // 将费用截止日期账单更新为不显示,截止日期后的账单删掉
+        List<ExpenseOrderDO> expenseOrderDOS = expenseOrderMapper.selectList(new LambdaQueryWrapperX<ExpenseOrderDO>()
+                .eq(ExpenseOrderDO::getElderId, elderlyInfo.getId())
+                .eq(ExpenseOrderDO::getPayStatus, 0)
+                .ge(ExpenseOrderDO::getBillingMonth, costDeadlineYearMonth));
+        for (ExpenseOrderDO expenseOrderDO : expenseOrderDOS) {
+            if(expenseOrderDO.getBillingMonth().equals(costDeadlineYearMonth.toString())){
+                costDeadLineExpenseOrder = expenseOrderDO;
+                expenseOrderDO.setIsShow(Boolean.FALSE);
+                expenseOrderMapper.updateById(expenseOrderDO);
+            }else {
+                expenseOrderService.deleteExpenseOrder(expenseOrderDO.getId());
+            }
+        }
+        // 解锁退住日期后续月账单,避免推送至金蝶
+        expenseOrderMapper.update(new ExpenseOrderDO().setIsLock(Boolean.FALSE),new LambdaQueryWrapperX<ExpenseOrderDO>()
                 .eq(ExpenseOrderDO::getElderId,elderlyInfo.getId())
                 .eq(ExpenseOrderDO::getPayStatus,0)
-                .ge(ExpenseOrderDO::getBillingMonth,costDeadlineYearMonth));
+                .eq(ExpenseOrderDO::getIsShow,Boolean.FALSE)
+                .gt(ExpenseOrderDO::getBillingMonth,costDeadlineYearMonth));
         if (elderlyCheckInRecordDO != null){
             elderlyCheckInRecordDO.setStatus(3);
             elderlyCheckInRecordMapper.updateById(elderlyCheckInRecordDO);
@@ -130,13 +159,17 @@ public class BpmElderlyExpenseApiImpl implements BpmElderlyExpenseApi {
         RefundSettlementOrderDO order = new RefundSettlementOrderDO();
         order.setElderId(elderlyInfo.getId());
         order.setBusinessId(businessId);
+        order.setVersion(1);
         order.setRefundAmount(BigDecimal.ZERO);
         order.setCreatedBy(SecurityFrameworkUtils.getLoginUserNickname());
         order.setCreatedTime(new Date());
         order.setOrderNumber("T" + RandomUtil.randomNumbers(15));
         order.setTenantId(startTenantId);
         refundSettlementOrderMapper.insert(order);
-
+        if(costDeadLineExpenseOrder != null){
+            costDeadLineExpenseOrder.setRefundSettlementOrderId(order.getId());
+        }
+//        createRetreatChangeDailyExpenses(elderlyRetreatRecord, startTenantId);
 
         // 生成退款单子项,获取退住结算数据
         RefundSettlementOrderRespVO virtualRefundSettlementOrder = refundSettlementOrderService.getVirtualRefundSettlementOrder(order.getId());
@@ -159,80 +192,6 @@ public class BpmElderlyExpenseApiImpl implements BpmElderlyExpenseApi {
         refundSettlementOrderMapper.updateById(order);
 
 
-        //查询长者月度费项配置
-        ExpenseDO expense = expenseMapper.selectList(new LambdaQueryWrapperX<ExpenseDO>()
-                        .eq(ExpenseDO::getElderId, elderlyInfo.getId())
-                        .eq(ExpenseDO::getTenantId, startTenantId)
-                        .orderByDesc(ExpenseDO::getCreatedTime))
-                .stream()
-                .findFirst()
-                .get();
-
-        List<ExpenseItemDO> expenseItems = expenseItemMapper.selectList(new LambdaQueryWrapperX<ExpenseItemDO>()
-                .eq(ExpenseItemDO::getExpenseId, expense.getId())
-                .eq(ExpenseItemDO::getIsMonthlyExpense, BooleanEnum.TRUE.getValue())
-                .eq(ExpenseItemDO::getTenantId, startTenantId));
-
-        for (ExpenseItemDO expenseItem : expenseItems) {
-            List<ExpenseItemDO> list = expenseItemMapper.selectCurrentMonthList(elderlyRetreatRecord.getRetreatDate(),
-                    elderlyRetreatRecord.getElderlyId(), expenseItem.getType());
-            LocalDate retreatDate = elderlyRetreatRecord.getRetreatDate();
-            LocalDate firstDayOfMonth = retreatDate.withDayOfMonth(1);
-            for (int i = 0; i < list.size(); i++) {
-                ExpenseItemDO expenseItemDO = list.get(i);
-                BigDecimal daysInCurrentMonth = BigDecimal.valueOf(elderlyRetreatRecord.getRetreatDate().lengthOfMonth());
-                BigDecimal actualAmount = expenseItemDO.getActualAmount();
-                LocalDate changeStartDate = expenseItemDO.getChangeStartDate();
-                LocalDate changeEndDate = expenseItemDO.getChangeEndDate();
-
-                if (null == changeEndDate && changeStartDate.isBefore(retreatDate)) {
-                    changeEndDate = retreatDate;
-                }
-
-                // 变更产生的金额
-                BigDecimal amount = BigDecimal.ZERO;
-                // 变更整月差额
-                BigDecimal changeAmount = BigDecimal.ZERO;
-                if (null != changeStartDate && null != changeEndDate) {
-                    long startDay = changeStartDate.toEpochDay();
-                    long endDay = changeEndDate.toEpochDay();
-                    BigDecimal currentDays = BigDecimal.valueOf(endDay - startDay);
-                    BigDecimal currentDayAmount = actualAmount.divide(daysInCurrentMonth, 8, RoundingMode.HALF_UP);
-                    amount = currentDayAmount.multiply(currentDays);
-                    if (i == list.size() - 1) {
-                        continue;
-                    }
-                    ExpenseItemDO nextItem = list.get(i + 1);
-                    LocalDate lastDayOfMonth = firstDayOfMonth.with(TemporalAdjusters.lastDayOfMonth());
-                    long epochDay = lastDayOfMonth.toEpochDay();
-                    BigDecimal changeDay = BigDecimal.valueOf(epochDay - endDay);
-                    BigDecimal changeDayAmount = nextItem.getActualAmount().divide(daysInCurrentMonth, 8, RoundingMode.HALF_UP);
-                    BigDecimal currentAmount = currentDayAmount.multiply(changeDay);
-                    changeAmount = changeDayAmount.multiply(changeDay);
-                    BigDecimal finalAmount = currentAmount.subtract(changeAmount);
-
-                    DailyExpensesDO dailyExpenses = new DailyExpensesDO();
-                    dailyExpenses.setType(2); // 2:变更
-                    dailyExpenses.setElderId(elderlyRetreatRecord.getElderlyId());
-                    dailyExpenses.setAttributionBillTime(lastDayOfMonth.format(DateTimeFormatter.ofPattern("yyyy-MM")));
-                    dailyExpenses.setItemId(nextItem.getId());
-                    dailyExpenses.setItemCategory(nextItem.getItemCategoryName());
-                    dailyExpenses.setItemName(nextItem.getItemName());
-                    dailyExpenses.setPrice(finalAmount);
-                    dailyExpenses.setAmount(finalAmount);
-                    String currentRemark = String.format(RETREAT_DAILY_PAY_REMARK, nextItem.getItemName(), changeEndDate
-                            ,changeEndDate, lastDayOfMonth);
-                    dailyExpenses.setRemarks(currentRemark); //todo 补充备注
-                    dailyExpenses.setCount(1);
-                    dailyExpenses.setCreatedTime(new Date());
-                    dailyExpenses.setAccountingTime(new Date());
-                    dailyExpenses.setIsGenerateBill(BooleanEnum.FALSE.getValue());
-                    dailyExpenses.setTenantId(startTenantId);
-                    dailyExpenses.setCreatedBy(SecurityFrameworkUtils.getLoginUserNickname());
-                    dailyExpensesMapper.insert(dailyExpenses);
-                }
-            }
-        }
         Map<String, Object> templateParams = new HashMap<>();
         templateParams.put("elderName", elderlyInfo.getElderName());
         List<BusinessParamsConfigDO> businessParamsConfigDOS = businessParamsConfigMapper.selectListByName(startTenantId ,NoticeConstants.REFUND_SETTLEMENT_HANDLE_NAME);
@@ -308,6 +267,85 @@ public class BpmElderlyExpenseApiImpl implements BpmElderlyExpenseApi {
 
     }
 
+    private void createRetreatChangeDailyExpenses(ElderlyRetreatRecordDO elderlyRetreatRecord, Long startTenantId) {
+        ExpenseDO expense = expenseMapper.selectList(new LambdaQueryWrapperX<ExpenseDO>()
+                        .eq(ExpenseDO::getElderId, elderlyRetreatRecord.getElderlyId())
+                        .eq(ExpenseDO::getTenantId, startTenantId)
+                        .orderByDesc(ExpenseDO::getCreatedTime))
+                .stream()
+                .findFirst()
+                .orElse(null);
+        if (expense == null) {
+            return;
+        }
+
+        List<ExpenseItemDO> expenseItems = expenseItemMapper.selectList(new LambdaQueryWrapperX<ExpenseItemDO>()
+                .eq(ExpenseItemDO::getExpenseId, expense.getId())
+                .eq(ExpenseItemDO::getIsMonthlyExpense, BooleanEnum.TRUE.getValue())
+                .eq(ExpenseItemDO::getTenantId, startTenantId));
+
+        for (ExpenseItemDO expenseItem : expenseItems) {
+            List<ExpenseItemDO> list = expenseItemMapper.selectCurrentMonthList(elderlyRetreatRecord.getRetreatDate(),
+                    elderlyRetreatRecord.getElderlyId(), expenseItem.getType());
+            LocalDate retreatDate = elderlyRetreatRecord.getRetreatDate();
+            LocalDate firstDayOfMonth = retreatDate.withDayOfMonth(1);
+            for (int i = 0; i < list.size(); i++) {
+                ExpenseItemDO expenseItemDO = list.get(i);
+                BigDecimal daysInCurrentMonth = BigDecimal.valueOf(elderlyRetreatRecord.getRetreatDate().lengthOfMonth());
+                BigDecimal actualAmount = expenseItemDO.getActualAmount();
+                LocalDate changeStartDate = expenseItemDO.getChangeStartDate();
+                LocalDate changeEndDate = expenseItemDO.getChangeEndDate();
+
+                if (changeStartDate == null) {
+                    continue;
+                }
+                if (changeEndDate == null && changeStartDate.isBefore(retreatDate)) {
+                    changeEndDate = retreatDate;
+                }
+                if (changeEndDate == null) {
+                    continue;
+                }
+                if (i == list.size() - 1) {
+                    continue;
+                }
+
+                long endDay = changeEndDate.toEpochDay();
+                ExpenseItemDO nextItem = list.get(i + 1);
+                LocalDate lastDayOfMonth = firstDayOfMonth.with(TemporalAdjusters.lastDayOfMonth());
+                long epochDay = lastDayOfMonth.toEpochDay();
+                BigDecimal changeDay = BigDecimal.valueOf(epochDay - endDay);
+                if (changeDay.compareTo(BigDecimal.ZERO) <= 0) {
+                    continue;
+                }
+
+                BigDecimal currentDayAmount = actualAmount.divide(daysInCurrentMonth, 8, RoundingMode.HALF_UP);
+                BigDecimal changeDayAmount = nextItem.getActualAmount().divide(daysInCurrentMonth, 8, RoundingMode.HALF_UP);
+                BigDecimal currentAmount = currentDayAmount.multiply(changeDay);
+                BigDecimal changeAmount = changeDayAmount.multiply(changeDay);
+                BigDecimal finalAmount = currentAmount.subtract(changeAmount);
+
+                DailyExpensesDO dailyExpenses = new DailyExpensesDO();
+                dailyExpenses.setType(2);
+                dailyExpenses.setElderId(elderlyRetreatRecord.getElderlyId());
+                dailyExpenses.setAttributionBillTime(lastDayOfMonth.format(DateTimeFormatter.ofPattern("yyyy-MM")));
+                dailyExpenses.setItemId(nextItem.getId());
+                dailyExpenses.setItemCategory(nextItem.getItemCategoryName());
+                dailyExpenses.setItemName(nextItem.getItemName());
+                dailyExpenses.setPrice(finalAmount);
+                dailyExpenses.setAmount(finalAmount);
+                String currentRemark = String.format(RETREAT_DAILY_PAY_REMARK, nextItem.getItemName(), changeEndDate, changeEndDate, lastDayOfMonth);
+                dailyExpenses.setRemarks(currentRemark);
+                dailyExpenses.setCount(1);
+                dailyExpenses.setCreatedTime(new Date());
+                dailyExpenses.setAccountingTime(new Date());
+                dailyExpenses.setIsGenerateBill(BooleanEnum.FALSE.getValue());
+                dailyExpenses.setTenantId(startTenantId);
+                dailyExpenses.setCreatedBy(SecurityFrameworkUtils.getLoginUserNickname());
+                dailyExpensesMapper.insert(dailyExpenses);
+            }
+        }
+    }
+
     @NotNull
     private static RefundSettlementOrderItemDO getRefundSettlementOrderItemDO(ExpenseOrderItemVO itemVO, RefundSettlementOrderDO order) {
         RefundSettlementOrderItemDO refundItem = new RefundSettlementOrderItemDO();
@@ -319,6 +357,8 @@ public class BpmElderlyExpenseApiImpl implements BpmElderlyExpenseApi {
         refundItem.setRefundAmount(itemVO.getAmount());
         refundItem.setActualRefundAmount(itemVO.getAmount());
         refundItem.setSourceItemId(itemVO.getItemId());
+        refundItem.setSourceOrderItemId(itemVO.getSourceOrderItemId());
+        refundItem.setExpenseSource(itemVO.getExpenseSource());
         refundItem.setTenantId(TenantContextHolder.getTenantId());
         // 根据费用类型设置type字段
         if (itemVO.getIsDeposit() != null && itemVO.getIsDeposit() == 1) {
@@ -343,23 +383,165 @@ public class BpmElderlyExpenseApiImpl implements BpmElderlyExpenseApi {
     }
 
     @Override
-    public void applyRetreatCheck(Long elderId,LocalDate costDeadlineDate){
+    @TenantIgnore
+    @Transactional
+    public void revokeApprovedRetreat(Long retreatRecordId) {
+        ElderlyRetreatRecordDO retreatRecord = retreatRecordMapper.selectById(retreatRecordId);
+        if (retreatRecord == null) {
+            throw new RuntimeException("找不到该退住申请记录");
+        }
+
+        RefundSettlementOrderDO refundSettlementOrder = refundSettlementOrderMapper.selectOne(new LambdaQueryWrapperX<RefundSettlementOrderDO>()
+                .eq(RefundSettlementOrderDO::getBusinessId, retreatRecordId)
+                .orderByDesc(RefundSettlementOrderDO::getCreatedTime)
+                .last("LIMIT 1"));
+        if (refundSettlementOrder == null) {
+            throw new RuntimeException("未找到对应退住结算单,无法撤销退住申请");
+        }
+        if (Objects.equals(refundSettlementOrder.getStatus(), BooleanEnum.TRUE.getValue())) {
+            throw new RuntimeException("退住结算单已结算,请先撤销结算");
+        }
+
+        List<RefundSettlementOrderItemDO> refundItems = refundSettlementOrderItemMapper.selectList(
+                new LambdaQueryWrapperX<RefundSettlementOrderItemDO>()
+                        .eq(RefundSettlementOrderItemDO::getRefundSettlementOrderId, refundSettlementOrder.getId()));
+        Set<Long> subsidyIds = new HashSet<>();
+        Set<Long> voucherIds = new HashSet<>();
+        for (RefundSettlementOrderItemDO refundItem : refundItems) {
+            if(refundItem.getExpenseSource().equals(BusinessConstants.LONG_TERM_CARE_INSURANCE)){
+                subsidyIds.add(refundItem.getSourceItemId());
+            }
+            if(refundItem.getExpenseSource().equals(BusinessConstants.CONSUMER_VOUCHER)){
+                voucherIds.add(refundItem.getSourceItemId());
+            }
+        }
+        List<ExpenseSubsidyDO> expenseSubsidyDOS = expenseSubsidyMapper.selectList(new LambdaQueryWrapperX<ExpenseSubsidyDO>().in(ExpenseSubsidyDO::getId, subsidyIds));
+        List<ExpenseSubsidyDO> updateExpenseSubsidyList = new ArrayList<>();
+        for (ExpenseSubsidyDO expenseSubsidyDO : expenseSubsidyDOS) {
+            // 退住后补录的长护险未带入到任何月度账单,需要撤销抵扣状态
+            if (StringUtils.isBlank(expenseSubsidyDO.getOrderNumber())) {
+                expenseSubsidyDO.setStatus(0);
+                updateExpenseSubsidyList.add(expenseSubsidyDO);
+            }
+        }
+        // 更新退住后补录的长护险
+        expenseSubsidyMapper.updateBatch(updateExpenseSubsidyList);
+
+        List<ElderlyConsumerVouchersDO> vouchersDOS = consumerVouchersMapper.selectList(new LambdaQueryWrapperX<ElderlyConsumerVouchersDO>().in(ElderlyConsumerVouchersDO::getId, voucherIds));
+        List<ElderlyConsumerVouchersDO> updatevouchersList = new ArrayList<>();
+        for (ElderlyConsumerVouchersDO vouchersDO : vouchersDOS) {
+            if (StringUtils.isBlank(vouchersDO.getOrderNumber())) {
+                vouchersDO.setStatus(0);
+                updatevouchersList.add(vouchersDO);
+            }
+        }
+        consumerVouchersMapper.updateBatch(updatevouchersList);
+
+
+        refundSettlementOrderItemMapper.delete(new LambdaQueryWrapperX<RefundSettlementOrderItemDO>()
+                .eq(RefundSettlementOrderItemDO::getRefundSettlementOrderId, refundSettlementOrder.getId()));
+        refundSettlementOrderMapper.deleteById(refundSettlementOrder.getId());
+
+        Long elderId = retreatRecord.getElderlyId();
+        YearMonth costDeadlineYearMonth = YearMonth.from(retreatRecord.getCostDeadlineDate());
+        expenseOrderMapper.update(new ExpenseOrderDO().setIsShow(Boolean.TRUE), new LambdaQueryWrapperX<ExpenseOrderDO>()
+                .eq(ExpenseOrderDO::getElderId, elderId)
+                .eq(ExpenseOrderDO::getPayStatus, 0)
+                .eq(ExpenseOrderDO::getIsShow, Boolean.FALSE)
+                .ge(ExpenseOrderDO::getBillingMonth, costDeadlineYearMonth));
+        notifyMessageSendApi.updateMessageByCodeAndPkMessage(BpmMessageEnum.REFUND_SETTLEMENT_HANDLE.getTemplateCode(), elderId);
+    }
+
+    @Override
+    public void applyRetreatCheck(Long elderId, LocalDate costDeadlineDate) {
+        applyRetreatCommonCheck(elderId, costDeadlineDate);
+        applyRetreatTempOutCheck(elderId);
+    }
+
+    /**
+     * 退住申请通用校验(普通退住与离住转退住均需校验)
+     * <p>
+     * 1. 费用截止日期月是否存在该长者账单(仅入住状态为在院长者校验)
+     * 2. 是否存在未返院的请假记录
+     */
+    public void applyRetreatCommonCheck(Long elderId, LocalDate costDeadlineDate) {
         ElderlyInfoDO elderlyInfoDO = elderlyInfoMapper.selectById(elderId);
-        if(elderlyInfoDO.getInStatusType() == 1){
+        if (elderlyInfoDO.getInStatusType() == 1) {
             YearMonth deadLineYearMonth = YearMonth.from(costDeadlineDate);
             List<ExpenseOrderDO> expenseOrderDOS = expenseOrderMapper.selectList(new LambdaQueryWrapperX<ExpenseOrderDO>()
                     .eq(ExpenseOrderDO::getElderId, elderId)
                     .eq(ExpenseOrderDO::getBillingMonth, deadLineYearMonth)
+                    .in(ExpenseOrderDO::getType, 1, 2)
             );
-            if(CollectionUtil.isEmpty(expenseOrderDOS)){
+            if (CollectionUtil.isEmpty(expenseOrderDOS)) {
                 throw exception(APPLY_RETREAT_CURRENT_EXPENSE_ORDER_NOT_EXISTS);
             }
         }
         List<ElderlyAskLeaveDO> elderlyAskLeaveDOS = askLeaveMapper.selectList(new LambdaQueryWrapperX<ElderlyAskLeaveDO>()
                 .eq(ElderlyAskLeaveDO::getElderId, elderId)
-                .isNull(ElderlyAskLeaveDO::getUpdateDate));
-        if(CollectionUtil.isNotEmpty(elderlyAskLeaveDOS)){
+                .isNull(ElderlyAskLeaveDO::getUpdateDate)
+                .and(w -> w.eq(ElderlyAskLeaveDO::getLeaveStatus, ElderlyAskLeaveDO.LEAVE_STATUS_APPROVED)
+                        .or().isNull(ElderlyAskLeaveDO::getLeaveStatus)));
+        if (CollectionUtil.isNotEmpty(elderlyAskLeaveDOS)) {
             throw exception(APPLY_RETREAT_ASK_LEAVE_EXISTS);
         }
     }
+
+    /**
+     * 退住申请专属校验(仅普通退住需校验,离住转退住跳过)
+     * <p>
+     * 检查是否存在未返院的离住记录
+     */
+    public void applyRetreatTempOutCheck(Long elderId) {
+        List<ElderlyTempOutDO> elderlyTempOutDOS = elderlyTempOutMapper.selectList(new LambdaQueryWrapperX<ElderlyTempOutDO>()
+                .eq(ElderlyTempOutDO::getElderId, elderId)
+                .eq(ElderlyTempOutDO::getOutStatus, 1));
+        if (CollectionUtil.isNotEmpty(elderlyTempOutDOS)) {
+            throw exception(APPLY_RETREAT_TEMP_OUT_EXISTS);
+        }
+    }
+
+    private void updateExpenseItemEndDate(Long elderId, LocalDate endDate) {
+        LambdaUpdateWrapper<ExpenseItemDO> updateWrapper = new LambdaUpdateWrapper<>();
+        updateWrapper
+                .eq(ExpenseItemDO::getIsMonthlyExpense, BooleanEnum.TRUE.getValue())
+                .isNull(ExpenseItemDO::getChangeEndDate)
+                .inSql(ExpenseItemDO::getExpenseId,
+                        "SELECT id FROM elderly_expense WHERE elder_id = " + elderId)
+                .set(ExpenseItemDO::getChangeEndDate, endDate);
+        expenseItemMapper.update(null, updateWrapper);
+    }
+
+    @Override
+    public void handleTempOutReturnForRetreat(Long tempOutId, Long elderId, Long tenantId, LocalDate costDeadlineDate) {
+        if (tempOutId == null || elderId == null || costDeadlineDate == null) {
+            return;
+        }
+        // 1. 对离住记录做返院登记,返院日期为费用截止日期+1,避免多收1天费用,后续恢复正常费用截止日期
+        ElderlyTempOutDO tempOutDO = elderlyTempOutMapper.selectById(tempOutId);
+        if (tempOutDO == null) {
+            return;
+        }
+        // 仅当该离住记录仍未返院时才登记,避免重复登记
+        if (tempOutDO.getOutStatus() != null && tempOutDO.getOutStatus() == 2) {
+            return;
+        }
+        YearMonth comeYm = YearMonth.from(costDeadlineDate);
+        ElderlyTempOutDO updateDO = new ElderlyTempOutDO();
+        updateDO.setId(tempOutId);
+        updateDO.setComeTime(costDeadlineDate.atStartOfDay().plusDays(1));
+        updateDO.setComeMonth(comeYm.toString());
+        updateDO.setOutStatus(2);
+        elderlyTempOutMapper.updateById(updateDO);
+        // 2. 费用截止日期当月若无月度账单,则生成该长者该月账单(参考 batchGenerateExpenseBill 单长者逻辑)
+        YearMonth billingMonth = YearMonth.from(costDeadlineDate);
+        ExpenseOrderSaveVO saveVO = new ExpenseOrderSaveVO();
+        saveVO.setTenantId(tenantId);
+        saveVO.setBillingMonth(billingMonth.toString());
+        saveVO.setElderId(elderId);
+        expenseOrderService.batchGenerateExpenseBill(saveVO, null);
+        // 3. 恢复正确返院日期
+        updateDO.setComeTime(costDeadlineDate.atStartOfDay());
+        elderlyTempOutMapper.updateById(updateDO);
+    }
 }

+ 190 - 293
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/kingdee/KingdeeApiImpl.java

@@ -40,13 +40,16 @@ import javax.annotation.Resource;
 import java.math.BigDecimal;
 import java.math.RoundingMode;
 import java.time.LocalDate;
+import java.time.YearMonth;
 import java.time.format.DateTimeFormatter;
 import java.util.*;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.Function;
 import java.util.stream.Collectors;
 
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exceptionCustomMsg;
 import static cn.iocoder.yudao.module.system.dal.dataobject.tenant.TenantDO.PACKAGE_ID_SYSTEM;
+import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.COMMON_ERROR;
 import static org.aspectj.bridge.MessageUtil.fail;
 
 /**
@@ -132,9 +135,7 @@ public class KingdeeApiImpl implements KingdeeApi {
             String FMAINBOOKSTDCURRID = "PRE001";
             String fid = "";
             List<ReceivableFEntityDetail> FEntityDetailList = new ArrayList<>();
-            List<ExpenseOrderItemDO> expenseOrderItemDOS = orderItemMapper.selectList(new LambdaQueryWrapperX<ExpenseOrderItemDO>()
-                    .eq(ExpenseOrderItemDO::getExpenseOrderId, expenseOrderDO.getId())
-                    .eq(ExpenseOrderItemDO::getTenantId, tenantId));
+            List<ExpenseOrderItemDO> expenseOrderItemDOS = orderItemMapper.selectListByOrderId(expenseOrderDO.getId(),tenantId);
             if (CollectionUtils.isEmpty(expenseOrderItemDOS)) {
                 return;
             }
@@ -142,6 +143,25 @@ public class KingdeeApiImpl implements KingdeeApi {
                 if(expenseOrderItemDO.getTotalAmount().compareTo(BigDecimal.ZERO) == 0){
                     return;
                 }
+                // 设施费不推送应收单
+                if(expenseOrderItemDO.getItemCategoryName() != null && expenseOrderItemDO.getItemCategoryName().contains("设施费")){
+                    expenseOrderDO.setActualAmount(expenseOrderDO.getActualAmount().subtract(expenseOrderItemDO.getTotalAmount()));
+                    return;
+                }
+                // 赠送不推送应收单
+                if((expenseOrderItemDO.getIsFreeGift() != null && expenseOrderItemDO.getIsFreeGift() == 1)){
+                    expenseOrderDO.setActualAmount(expenseOrderDO.getActualAmount().subtract(expenseOrderItemDO.getTotalAmount()));
+                    return;
+                }
+                // 押金不推送应收单
+                if((expenseOrderItemDO.getIsDeposit() != null && expenseOrderItemDO.getIsDeposit() == 1) ||
+                        (expenseOrderItemDO.getItemCategoryName() != null &&
+                                (expenseOrderItemDO.getItemCategoryName().contains("押金") ||
+                                expenseOrderItemDO.getItemCategoryName().contains("预备金") ||
+                                expenseOrderItemDO.getItemCategoryName().contains("保证金")))){
+                    expenseOrderDO.setActualAmount(expenseOrderDO.getActualAmount().subtract(expenseOrderItemDO.getTotalAmount()));
+                    return;
+                }
                 // 床位费 CI047
                 // 膳食费 CI048
                 // 护理费 CI050
@@ -153,6 +173,11 @@ public class KingdeeApiImpl implements KingdeeApi {
                 if(StringUtils.isBlank(FCOSTID)){
                     return;
                 }
+                // 补上长护险费用,还是应该要收的
+                if(FCOSTID.equals("CI051")){
+                    expenseOrderDO.setActualAmount(expenseOrderDO.getActualAmount().subtract(expenseOrderItemDO.getTotalAmount()));
+                    return;
+                }
                 // 老人公寓四舍五入调整
                 BigDecimal roundAmount = expenseOrderItemDO.getTotalAmount();
                 String count = expenseOrderItemDO.getCount().toString();
@@ -168,24 +193,24 @@ public class KingdeeApiImpl implements KingdeeApi {
                         .build();
                 FEntityDetailList.add(entityDetail);
             });
-            // date PayTime转换成yyyy-MM-dd字符串
-            String payTimeStr = DateUtils.formatDate(new Date(), DateUtils.FORMAT_YEAR_MONTH_DAY);
+            BigDecimal totalAmount = FEntityDetailList.stream().map(ReceivableFEntityDetail::getFALLAMOUNTFOR_D).reduce(BigDecimal.ZERO, BigDecimal::add);
+            YearMonth yearMonth = YearMonth.parse(expenseOrderDO.getBillingMonth());
             ReceivableFsubHeadFinc headFinc = ReceivableFsubHeadFinc.builder()
-                    .FACCNTTIMEJUDGETIME(payTimeStr)
+                    .FACCNTTIMEJUDGETIME(yearMonth.atEndOfMonth().toString())
                     .FMAINBOOKSTDCURRID(KingdeeNumberDTO.builder().FNUMBER(FMAINBOOKSTDCURRID).build())
                     .FEXCHANGETYPE(KingdeeNumberDTO.builder().FNUMBER("HLTX01_SYS").build())
                     .FExchangeRate(new BigDecimal("1.0"))
                     .FTaxAmountFor(new BigDecimal("0.0"))
-                    .FNoTaxAmountFor(expenseOrderDO.getActualAmount())
+                    .FNoTaxAmountFor(totalAmount)
                     .FISCARRIEDDATE(Boolean.FALSE.toString())
                     .build();
             ReceivableSaveModelDTO saveModelDTO = ReceivableSaveModelDTO.builder()
                     .FID(fid)
                     .FBillTypeID(KingdeeNumberDTO.builder().FNUMBER("YSD02_SYS").build())
                     .FBillNo(expenseOrderDO.getBillOrderNumber())
-                    .FDATE(payTimeStr)
+                    .FDATE(yearMonth.atEndOfMonth().toString())
                     .FISINIT(Boolean.FALSE.toString())
-                    .FENDDATE_H(expenseOrderDO.getBillingMonth())
+                    .FENDDATE_H(yearMonth.atEndOfMonth().toString())
                     .FSETTLEORGID(KingdeeNumberDTO.builder().FNUMBER(orgId).build())
                     .FCUSTOMERID(KingdeeNumberDTO.builder().FNUMBER(customerid).build())
                     .FCURRENCYID(KingdeeNumberDTO.builder().FNUMBER(FMAINBOOKSTDCURRID).build())
@@ -242,194 +267,6 @@ public class KingdeeApiImpl implements KingdeeApi {
         return expenseOrderDOS.size();
     }
 
-    @Override
-    @TenantIgnore
-    public Integer syncArReceivebill(Long tenantId) {
-        Integer payStatus = 1;
-        List<ExpenseOrderDO> expenseOrderDOS = orderMapper.selectList(new LambdaQueryWrapperX<ExpenseOrderDO>()
-                .eq(ExpenseOrderDO::getPayStatus, payStatus)
-                .eq(ExpenseOrderDO::getBillingMonth, "2025-05")
-                .eq(ExpenseOrderDO::getTenantId, tenantId));
-        if (null == expenseOrderDOS) {
-            return 0;
-        }
-
-        // 根据elderId , billingMonth ,payStatus比较相同,保留createdTime最新的数据
-        List<ExpenseOrderDO> deduplicate = deduplicate(expenseOrderDOS);
-
-        for (ExpenseOrderDO expenseOrderDO : deduplicate) {
-            if (expenseOrderDO.getBillingMonth().compareTo("2024-11") < 0) {
-                continue;
-            }
-            String payTimeStr = DateUtils.formatDate(expenseOrderDO.getPayTime(), DateUtils.FORMAT_YEAR_MONTH_DAY);
-            ElderlyInfoDO elderlyInfoDO = elderlyInfoMapper.selectById(expenseOrderDO.getElderId());
-            if (null == elderlyInfoDO) continue;
-            if (StringUtil.isEmptyORNull(elderlyInfoDO.getKingdeeCustomerNumber())) continue;
-            if (!elderlyInfoDO.getElderName().equals("吕福元")) continue;
-            TenantDO tenantDO = tenantMapper.selectById(expenseOrderDO.getTenantId());
-//            String customerid = StringUtil.isNotEmptyORNull(elderlyInfoDO.getCustomerid()) ? elderlyInfoDO.getCustomerid() : "CUST0002";
-            String customerid = StringUtil.isNotEmptyORNull(elderlyInfoDO.getKingdeeCustomerNumber()) ? elderlyInfoDO.getKingdeeCustomerNumber() : "CUST0002";
-
-            String tenantid = StringUtil.isNotEmptyORNull(tenantDO.getKingdeeOrgid()) ? tenantDO.getKingdeeOrgid() : "106";
-
-            String  FBillNo = "BILL" + expenseOrderDO.getBillOrderNumber();
-
-            // 应收单默认货币
-            AtomicReference<String> FMAINBOOKSTDCURRID = new AtomicReference<>("PRE001");
-            List<KingdeeRecordDO> kingdeeRecordDOS = kingdeeRecordMapper.selectList(new LambdaQueryWrapperX<KingdeeRecordDO>()
-                    .eq(KingdeeRecordDO::getFBillNo, FBillNo)
-                    .eq(KingdeeRecordDO::getSyncState, 1)
-                    .orderByDesc(KingdeeRecordDO::getId).last("LIMIT 1"));
-            String fid = "";
-            if (null != kingdeeRecordDOS && kingdeeRecordDOS.size() > 0) {
-                KingdeeRecordDO kingdeeRecordDO = kingdeeRecordDOS.get(0);
-                fid = kingdeeRecordDO.getFid();
-            }
-            List<ReceiveBillSaveEntryDTO> freceivebillentryList = new ArrayList<>();
-
-            List<ExpenseOrderItemDO> expenseOrderItemDOS = orderItemMapper.selectList(new LambdaQueryWrapperX<ExpenseOrderItemDO>()
-                    .eq(ExpenseOrderItemDO::getExpenseOrderId, expenseOrderDO.getId())
-                    .eq(ExpenseOrderItemDO::getPayStatus, expenseOrderDO.getPayStatus()));
-            if (null == expenseOrderItemDOS) {
-                continue;
-            }
-
-
-            expenseOrderItemDOS.forEach(expenseOrderItemDO -> {
-                // 费用类型
-                // 床位费 CI047
-                // 膳食费 CI109
-                // 护理费 CI111
-
-                // 风险预备金 CI002
-                // 保证金 CI003
-                String FCOSTID = "CI047";
-                String FSETTLETYPEID = "JSFS04_SYS";
-                String FPURPOSEID = "SFKYT01_SYS";
-//                String FACCOUNTID = "398460100100148998";
-//                String FACCOUNTID = "398460100100153498";
-                String FACCOUNTID = "713373400267";
-
-                if (BusinessConstants.EXPENSE_ITEM.equals(expenseOrderItemDO.getExpenseSource())) {
-                    ExpenseItemDO expenseItemDO = expenseItemMapper.selectById(expenseOrderItemDO.getSourceExpenseItemId());
-                    if (null == expenseItemDO) return;
-                    OverheadChargeDO overheadChargeDO = overheadChargeMapper.selectById(expenseItemDO.getItemId());
-                    if (null != overheadChargeDO && StringUtil.isNotEmptyORNull(overheadChargeDO.getKingdeeCostid())) {
-                        FCOSTID = overheadChargeDO.getKingdeeCostid();
-                    }
-                }
-
-                ExpensePayOrderDO expensePayOrderDO = payOrderMapper.selectById(expenseOrderItemDO.getPayOrderId());
-                if (null != expensePayOrderDO) {
-                    List<ExpensePayOrderItemDO> expensePayOrderItemDOS = payOrderItemMapper.selectList(new LambdaQueryWrapperX<ExpensePayOrderItemDO>()
-                            .eq(ExpensePayOrderItemDO::getPayOrderId, expensePayOrderDO.getId())
-                            .eq(ExpensePayOrderItemDO::getTenantId, tenantId).orderByAsc(ExpensePayOrderItemDO::getId));
-                    if (null != expensePayOrderItemDOS && expensePayOrderItemDOS.size() > 0) {
-                        ExpensePayOrderItemDO expensePayOrderItemDO = expensePayOrderItemDOS.get(0);
-                        if (StringUtil.isNotEmptyORNull(expensePayOrderItemDO.getCurrencyCode())) {
-                            FMAINBOOKSTDCURRID.set(expensePayOrderItemDO.getCurrencyCode());
-                        }
-                        if (StringUtil.isNotEmptyORNull(expensePayOrderItemDO.getBankAccount())) {
-                            FACCOUNTID = expensePayOrderItemDO.getBankAccount();
-                        }
-                    }
-                }
-
-                BigDecimal roundAmount = expenseOrderItemDO.getRoundAmount();
-                if (null == roundAmount) return;
-
-                String count = expenseOrderItemDO.getCount().toString();
-                // 判断是负数
-                if (roundAmount.compareTo(BigDecimal.ZERO) < 0) {
-                    count = "-" + count;
-                }
-                DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DateUtils.FORMAT_YEAR_MONTH_DAY);
-                LocalDate billingDate = LocalDate.parse(expenseOrderDO.getBillingMonth() + "-01", formatter);
-                LocalDate lastDayOfMonth = billingDate.withDayOfMonth(billingDate.lengthOfMonth());
-                // 转换成String
-                String lastDayOfMonthStr = lastDayOfMonth.format(formatter);
-                ReceiveBillSaveEntryDTO freceivebillentry = ReceiveBillSaveEntryDTO.builder()
-                        .FSETTLETYPEID(KingdeeNumberDTO.builder().FNUMBER(FSETTLETYPEID).build())
-                        .FPURPOSEID(KingdeeNumberDTO.builder().FNUMBER(FPURPOSEID).build())
-                        .FRECTOTALAMOUNTFOR(roundAmount)
-                        .FRECAMOUNTFOR_E(roundAmount)
-                        .FRECNOTAXAMOUNTFOR(roundAmount)
-                        .FSETTLEDISTAMOUNTFOR(new BigDecimal(0.0))
-                        .FHANDLINGCHARGEFOR(new BigDecimal(0.0))
-                        .FOVERUNDERAMOUNTFOR(new BigDecimal(0.0))
-                        .FACCOUNTID(KingdeeNumberDTO.builder().FNUMBER(FACCOUNTID).build())
-                        .FOPPOSITEBANKACCOUNT("")
-                        .FOPPOSITEBANKNAME("")
-                        .FPrice(expenseOrderItemDO.getPrice())
-                        .FQty(expenseOrderItemDO.getCount())
-                        .FPOSTDATE(lastDayOfMonthStr)
-                        .FRECNOTAXAMOUNTFOR(roundAmount)
-                        .FRECNOTAXAMOUNT(roundAmount)
-                        .FWRITTENOFFTAXAMOUNTFOR(roundAmount)
-                        .FWRITTENOFFTAXAMOUNT(roundAmount)
-                        .FCOSTID(KingdeeNumberDTO.builder().FNUMBER(FCOSTID).build())
-                        .FCOMMENT(expenseOrderItemDO.getItemName())
-                        .build();
-                freceivebillentryList.add(freceivebillentry);
-            });
-
-
-            ReceiveBillSaveModelDTO saveModelDTO = ReceiveBillSaveModelDTO.builder()
-                    .FID(fid)
-                    .FBillNo(FBillNo)
-                    .FBillTypeID(KingdeeNumberDTO.builder().FNUMBER("SKDLX01_SYS").build())
-                    .FPAYORGID(KingdeeNumberDTO.builder().FNUMBER(tenantid).build())
-//                    .FSETTLEORGID(KingdeeNumberDTO.builder().FNUMBER(tenantid).build())
-//                    .FSALEORGID(KingdeeNumberDTO.builder().FNUMBER(tenantid).build())
-                    .FDATE(expenseOrderDO.getBillingMonth())
-                    .FCONTACTUNITTYPE("BD_Customer")
-                    .FCONTACTUNIT(KingdeeNumberDTO.builder().FNUMBER(customerid).build())
-                    .FPAYUNITTYPE("BD_Customer")
-                    .FPAYUNIT(KingdeeNumberDTO.builder().FNUMBER(customerid).build())
-                    .FCURRENCYID(KingdeeNumberDTO.builder().FNUMBER(FMAINBOOKSTDCURRID.get()).build())
-                    .FEXCHANGERATE(new BigDecimal("1.0"))
-                    .FSETTLECUR(KingdeeNumberDTO.builder().FNUMBER(FMAINBOOKSTDCURRID.get()).build())
-                    .FSETTLERATE(new BigDecimal("1.0"))
-                    .FISINIT(Boolean.FALSE.toString())
-                    .FISCARRYRATE(Boolean.FALSE.toString())
-                    .FDOCUMENTSTATUS("Z")
-                    .FBUSINESSTYPE("1")
-                    .FCancelStatus("A")
-                    .FSETTLEMAINBOOKID(KingdeeNumberDTO.builder().FNUMBER(FMAINBOOKSTDCURRID.get()).build())
-                    .FRECEIVEBILLENTRY(freceivebillentryList)
-
-                    .FBUSINESSTYPE("FY")
-
-
-                    .build();
-
-            ReceiveBillSaveDTO saveDTO = ReceiveBillSaveDTO.builder()
-                    .IsDeleteEntry(Boolean.TRUE.toString())
-                    .IsVerifyBaseDataField(Boolean.FALSE.toString())
-                    .IsEntryBatchFill(Boolean.TRUE.toString())
-                    .ValidateFlag(Boolean.TRUE.toString())
-                    .NumberSearch(Boolean.TRUE.toString())
-                    .IsAutoAdjustField(Boolean.FALSE.toString())
-                    .InterationFlags("")
-                    .IgnoreInterationFlag("")
-                    .IsControlPrecision(Boolean.FALSE.toString())
-                    .ValidateRepeatJson(Boolean.FALSE.toString())
-                    .model(saveModelDTO)
-                    .build();
-
-            Gson gson = new Gson();
-            // 将ReceivableSaveDTO转换成json字符串
-            String json = gson.toJson(saveDTO);
-            System.out.println("json = " + json);
-            try {
-                String billId = arReceivebillSave(getK3CloudApi(tenantId), json, expenseOrderDO.getBillOrderNumber(), expenseOrderDO.getElderId(),tenantId);
-            } catch (Exception e) {
-                e.printStackTrace();
-            }
-        }
-
-        return null;
-    }
     @Override
     @TenantIgnore
     public String syncArReceivebillNewBatch(List<Long> expenseOrderIdList) {
@@ -453,146 +290,206 @@ public class KingdeeApiImpl implements KingdeeApi {
             ElderlyInfoDO elderlyInfoDO = elderlyInfoMapper.selectById(expenseOrderDO.getElderId());
             if (null == elderlyInfoDO) continue;
             Long tenantId = expenseOrderDO.getTenantId();
-            if(tenantId == null){
+            if (tenantId == null) {
                 msg.append(elderlyInfoDO.getElderName()).append(",").append(expenseOrderDO.getBillingMonth()).append("账单不存在机构id<br/>");
                 continue;
             }
-            if(expenseOrderDO.getIsPushKingdeeReceiveBill()){
+            if (expenseOrderDO.getIsPushKingdeeReceiveBill()) {
                 msg.append(elderlyInfoDO.getElderName()).append(",").append(expenseOrderDO.getBillingMonth()).append("账单已经推送过金蝶收款单<br/>");
                 continue;
             }
             Long elderId = elderlyInfoDO.getId();
-            if (StringUtil.isEmptyORNull(elderlyInfoDO.getKingdeeCustomerNumber())) continue;
+            if (StringUtil.isEmptyORNull(elderlyInfoDO.getKingdeeCustomerNumber())) {
+                msg.append(elderlyInfoDO.getElderName()).append("该长者金蝶编号为空\n");
+                continue;
+            }
             TenantDO tenantDO = tenantMapper.selectById(expenseOrderDO.getTenantId());
             String kingedeeCustomerId = elderlyInfoDO.getKingdeeCustomerNumber();
-            String  FBillNo = "BILL" + expenseOrderDO.getBillOrderNumber();
+            String baseBillNo = "BILL" + expenseOrderDO.getBillOrderNumber();
             String kingedeeTenantId = tenantDO.getKingdeeOrgid();
-            // 应收单默认货币
-            AtomicReference<String> FMAINBOOKSTDCURRID = new AtomicReference<>("PRE001");
-            String fid = "";
-            List<ReceiveBillSaveEntryDTO> freceivebillentryList = new ArrayList<>();
-            // 费用类型
-            // 床位费 C0001
-            // 膳食费 CI048
-            // 护理费 CI050
-            // 长护险 CI051
-            // 服务费 CI049
-            // 其他 CI046
-            // 风险预备金 CI002
-            String FSETTLETYPEID = "JSFS04_SYS";
-            String FPURPOSEID = "SFKYT01_SYS";
+            String currency = "PRE001";
+
             List<EnterpriseBankInfoDO> openAccountList = enterpriseBankInfoMapper.selectList(new LambdaQueryWrapperX<EnterpriseBankInfoDO>()
                     .eq(EnterpriseBankInfoDO::getTenantId, tenantId));
             String FACCOUNTID = "";
-            if (null != openAccountList && openAccountList.size() > 0) {
-                FACCOUNTID = openAccountList.get(0).getAccountName();
-            } else {
-                FACCOUNTID = "398460100100153498";
+            if (null != openAccountList && !openAccountList.isEmpty()) {
+                FACCOUNTID = openAccountList.get(0).getBankAccount();
+            }
+            if (StringUtils.isBlank(FACCOUNTID)) {
+                throw exceptionCustomMsg(COMMON_ERROR, "不存在银行账户,请联系管理员处理");
             }
-//            BigDecimal payAmount = expenseOrderDO.getPayableAmount();
+
+            BigDecimal normalAmount = BigDecimal.ZERO;
+            BigDecimal facilityAmount = BigDecimal.ZERO;
+            BigDecimal depositAmount = BigDecimal.ZERO;
+
             List<ExpenseOrderItemDO> expenseOrderItemList = orderItemMapper.selectList(new LambdaQueryWrapperX<ExpenseOrderItemDO>()
                     .eq(ExpenseOrderItemDO::getExpenseOrderId, expenseOrderDO.getId())
                     .eq(ExpenseOrderItemDO::getPayStatus, BooleanEnum.TRUE.getValue()));
+
             for (ExpenseOrderItemDO expenseOrderItemDO : expenseOrderItemList) {
-                if(expenseOrderItemDO.getTotalAmount().compareTo(BigDecimal.ZERO) == 0){
+                if (expenseOrderItemDO.getTotalAmount().compareTo(BigDecimal.ZERO) == 0) {
+                    continue;
+                }
+                if (BusinessConstants.LONG_TERM_CARE_INSURANCE.equals(expenseOrderItemDO.getExpenseSource())) {
                     continue;
                 }
-                if(BusinessConstants.LONG_TERM_CARE_INSURANCE.equals(expenseOrderItemDO.getExpenseSource())){
+                String itemName = StringUtils.defaultString(expenseOrderItemDO.getItemName());
+                if (itemName.contains("设施费")) {
+                    facilityAmount = facilityAmount.add(expenseOrderItemDO.getTotalAmount());
                     continue;
                 }
                 String FCOSTID = getFCOSTID(expenseOrderItemDO);
-                if(StringUtils.isBlank(FCOSTID)){
+                if ("CI002".equals(FCOSTID)
+                        || itemName.contains("风险金")
+                        || itemName.contains("预备金")
+                        || itemName.contains("保证金")
+                        || itemName.contains("押金")) {
+                    depositAmount = depositAmount.add(expenseOrderItemDO.getTotalAmount());
                     continue;
                 }
-                ReceiveBillSaveEntryDTO freceivebillentry = ReceiveBillSaveEntryDTO.builder()
-                        .FSETTLETYPEID(KingdeeNumberDTO.builder().FNUMBER(FSETTLETYPEID).build())
-                        .FPURPOSEID(KingdeeNumberDTO.builder().FNUMBER(FPURPOSEID).build())
-                        .FRECTOTALAMOUNTFOR(expenseOrderItemDO.getTotalAmount())
-                        .FRECAMOUNTFOR_E(expenseOrderItemDO.getTotalAmount())
-                        .FRECNOTAXAMOUNTFOR(expenseOrderItemDO.getTotalAmount())
-                        .FSETTLEDISTAMOUNTFOR(new BigDecimal(0.0))
-                        .FHANDLINGCHARGEFOR(new BigDecimal(0.0))
-                        .FOVERUNDERAMOUNTFOR(new BigDecimal(0.0))
-                        .FACCOUNTID(KingdeeNumberDTO.builder().FNUMBER(FACCOUNTID).build())
-                        .FOPPOSITEBANKACCOUNT("")
-                        .FOPPOSITEBANKNAME("")
-                        .FPrice(expenseOrderItemDO.getActualPrice())
-                        .FQty(expenseOrderItemDO.getCount())
-                        .FPOSTDATE(payTimeStr)
-                        .FRECNOTAXAMOUNTFOR(expenseOrderItemDO.getTotalAmount())
-                        .FRECNOTAXAMOUNT(expenseOrderItemDO.getTotalAmount())
-                        .FWRITTENOFFTAXAMOUNTFOR(expenseOrderItemDO.getTotalAmount())
-                        .FWRITTENOFFTAXAMOUNT(expenseOrderItemDO.getTotalAmount())
-                        .FCOSTID(KingdeeNumberDTO.builder().FNUMBER(FCOSTID).build())
-                        .FCOMMENT("")
-                        .build();
-                freceivebillentryList.add(freceivebillentry);
+                normalAmount = normalAmount.add(expenseOrderItemDO.getTotalAmount());
             }
-            ReceiveBillSaveModelDTO saveModelDTO = ReceiveBillSaveModelDTO.builder()
-                    .FID(fid)
-                    .FBillNo(FBillNo)
-                    .FBillTypeID(KingdeeNumberDTO.builder().FNUMBER("SKDLX01_SYS").build())
-                    .FPAYORGID(KingdeeNumberDTO.builder().FNUMBER(kingedeeTenantId).build())
-                    .FDATE(payTimeStr)
-                    .FCONTACTUNITTYPE("BD_Customer")
-                    .FCONTACTUNIT(KingdeeNumberDTO.builder().FNUMBER(kingedeeCustomerId).build())
-                    .FPAYUNITTYPE("BD_Customer")
-                    .FPAYUNIT(KingdeeNumberDTO.builder().FNUMBER(kingedeeCustomerId).build())
-                    .FCURRENCYID(KingdeeNumberDTO.builder().FNUMBER(FMAINBOOKSTDCURRID.get()).build())
-                    .FEXCHANGERATE(new BigDecimal("1.0"))
-                    .FSETTLECUR(KingdeeNumberDTO.builder().FNUMBER(FMAINBOOKSTDCURRID.get()).build())
-                    .FSETTLERATE(new BigDecimal("1.0"))
-                    .FISINIT(Boolean.FALSE.toString())
-                    .FISCARRYRATE(Boolean.FALSE.toString())
-                    .FDOCUMENTSTATUS("Z")
-                    .FBUSINESSTYPE("1")
-                    .FCancelStatus("A")
-                    .FSETTLEMAINBOOKID(KingdeeNumberDTO.builder().FNUMBER(FMAINBOOKSTDCURRID.get()).build())
-                    .FRECEIVEBILLENTRY(freceivebillentryList)
-                    .FBUSINESSTYPE("FY")
-                    .build();
-            ReceiveBillSaveDTO saveDTO = ReceiveBillSaveDTO.builder()
-                    .IsDeleteEntry(Boolean.TRUE.toString())
-                    .IsVerifyBaseDataField(Boolean.FALSE.toString())
-                    .IsEntryBatchFill(Boolean.TRUE.toString())
-                    .ValidateFlag(Boolean.TRUE.toString())
-                    .NumberSearch(Boolean.TRUE.toString())
-                    .IsAutoAdjustField(Boolean.FALSE.toString())
-                    .InterationFlags("")
-                    .IgnoreInterationFlag("")
-                    .IsControlPrecision(Boolean.FALSE.toString())
-                    .ValidateRepeatJson(Boolean.FALSE.toString())
-                    .model(saveModelDTO)
-                    .build();
-            Gson gson = new Gson();
-            // 将ReceivableSaveDTO转换成json字符串
-            String json = gson.toJson(saveDTO);
-            System.out.println("json = " + json);
+
+            boolean needPushNormal = normalAmount.compareTo(BigDecimal.ZERO) != 0;
+            boolean needPushFacility = facilityAmount.compareTo(BigDecimal.ZERO) != 0;
+            boolean needPushDeposit = depositAmount.compareTo(BigDecimal.ZERO) != 0;
+            if (!needPushNormal && !needPushFacility && !needPushDeposit) {
+                msg.append(elderlyInfoDO.getElderName()).append(",").append(expenseOrderDO.getBillingMonth()).append("账单无可推送收款明细<br/>");
+                continue;
+            }
+            String remark = "";
+            boolean allSuccess = true;
             try {
-                String billId = arReceivebillSave(getK3CloudApi(tenantId), json, expenseOrderDO.getBillOrderNumber(), elderId,tenantId);
+                if (needPushNormal) {
+                    YearMonth billingMonth = YearMonth.parse(expenseOrderDO.getBillingMonth());
+                    remark = "生活费" + billingMonth.getMonthValue() + "月";
+                    List<ReceiveBillSaveEntryDTO> normalEntryList = Collections.singletonList(
+                            buildReceiveBillEntry(FACCOUNTID, payTimeStr, normalAmount, "CI001", "SFKYT01_SYS", remark)
+                    );
+                    String billId = pushReceiveBill(tenantId, elderId, expenseOrderDO.getBillOrderNumber(), baseBillNo,
+                            "SKDLX01_SYS", kingedeeTenantId, kingedeeCustomerId, payTimeStr, currency, normalEntryList,remark);
+                    if (StringUtils.isBlank(billId)) {
+                        allSuccess = false;
+                    }
+                }
+
+                if (needPushFacility) {
+                    remark = "一次性设施购置费";
+                    List<ReceiveBillSaveEntryDTO> facilityEntryList = Collections.singletonList(
+                            buildReceiveBillEntry(FACCOUNTID, payTimeStr, facilityAmount, "CI005", "SFKYT02_SYS", remark)
+                    );
+                    String billId = pushReceiveBill(tenantId, elderId, expenseOrderDO.getBillOrderNumber() + "-SSF", baseBillNo + "-SSF",
+                            "SKDLX01_SYS", kingedeeTenantId, kingedeeCustomerId, payTimeStr, currency, facilityEntryList,remark);
+                    if (StringUtils.isBlank(billId)) {
+                        allSuccess = false;
+                    }
+                }
+
+                if (needPushDeposit) {
+                    remark = "医疗押金";
+                    List<ReceiveBillSaveEntryDTO> depositEntryList = Collections.singletonList(
+                            buildReceiveBillEntry(FACCOUNTID, payTimeStr, depositAmount, "CI002", "SFKYT41_SYS", remark)
+                    );
+                    String billId = pushReceiveBill(tenantId, elderId, expenseOrderDO.getBillOrderNumber() + "-YJ", baseBillNo + "-YJ",
+                            "SKDLX02_SYS", kingedeeTenantId, kingedeeCustomerId, payTimeStr, currency, depositEntryList,remark);
+                    if (StringUtils.isBlank(billId)) {
+                        allSuccess = false;
+                    }
+                }
+
                 msg.append(elderlyInfoDO.getElderName()).append(",").append(expenseOrderDO.getBillingMonth()).append("账单推送金蝶收款单");
-                if(StringUtils.isBlank(billId)){
+                if (!allSuccess) {
                     msg.append("失败。<br/>");
-                }else {
+                } else {
                     msg.append("成功。<br/>");
                     expenseOrderDO.setIsPushKingdeeReceiveBill(Boolean.TRUE);
                     successSet.add(expenseOrderDO);
                 }
             } catch (Exception e) {
-                // 把异常前的成功记录更新为已推送
-                if(successSet.size() > 0){
+                if (successSet.size() > 0) {
                     orderMapper.updateBatch(successSet);
                 }
                 e.printStackTrace();
             }
         }
-        // 把异常前的成功记录更新为已推送
-        if(successSet.size() > 0){
+        if (successSet.size() > 0) {
             orderMapper.updateBatch(successSet);
         }
         return msg.toString();
     }
 
+    private ReceiveBillSaveEntryDTO buildReceiveBillEntry(String accountId, String payTimeStr, BigDecimal amount,
+                                                          String costId, String purposeId, String comment) {
+        return ReceiveBillSaveEntryDTO.builder()
+                .FSETTLETYPEID(KingdeeNumberDTO.builder().FNUMBER("JSFS04_SYS").build())
+                .FPURPOSEID(KingdeeNumberDTO.builder().FNUMBER(purposeId).build())
+                .FRECTOTALAMOUNTFOR(amount)
+                .FRECAMOUNTFOR_E(amount)
+                .FRECNOTAXAMOUNTFOR(amount)
+                .FSETTLEDISTAMOUNTFOR(BigDecimal.ZERO)
+                .FHANDLINGCHARGEFOR(BigDecimal.ZERO)
+                .FOVERUNDERAMOUNTFOR(BigDecimal.ZERO)
+                .FACCOUNTID(KingdeeNumberDTO.builder().FNUMBER(accountId).build())
+                .FOPPOSITEBANKACCOUNT("")
+                .FOPPOSITEBANKNAME("")
+                .FPrice(amount)
+                .FQty(1)
+                .FPOSTDATE(payTimeStr)
+                .FRECNOTAXAMOUNT(amount)
+                .FWRITTENOFFTAXAMOUNTFOR(amount)
+                .FWRITTENOFFTAXAMOUNT(amount)
+                .FCOSTID(KingdeeNumberDTO.builder().FNUMBER(costId).build())
+                .F_PAEZ_Base(KingdeeNumberDTO.builder().FNUMBER(costId).build())
+                .FCOMMENT(comment)
+                .build();
+    }
+
+    private String pushReceiveBill(Long tenantId, Long elderId, String recordBillNo, String kdBillNo,
+                                   String billTypeId, String kingedeeTenantId, String kingedeeCustomerId, String payTimeStr,
+                                   String currency, List<ReceiveBillSaveEntryDTO> entryList,String remark) throws Exception {
+        ReceiveBillSaveModelDTO saveModelDTO = ReceiveBillSaveModelDTO.builder()
+                .FID("")
+                .FBillNo(kdBillNo)
+                .FBillTypeID(KingdeeNumberDTO.builder().FNUMBER(billTypeId).build())
+                .FPAYORGID(KingdeeNumberDTO.builder().FNUMBER(kingedeeTenantId).build())
+                .FDATE(payTimeStr)
+                .FCONTACTUNITTYPE("BD_Customer")
+                .FCONTACTUNIT(KingdeeNumberDTO.builder().FNUMBER(kingedeeCustomerId).build())
+                .FPAYUNITTYPE("BD_Customer")
+                .FPAYUNIT(KingdeeNumberDTO.builder().FNUMBER(kingedeeCustomerId).build())
+                .FCURRENCYID(KingdeeNumberDTO.builder().FNUMBER(currency).build())
+                .FEXCHANGERATE(new BigDecimal("1.0"))
+                .FSETTLECUR(KingdeeNumberDTO.builder().FNUMBER(currency).build())
+                .FSETTLERATE(new BigDecimal("1.0"))
+                .FISINIT(Boolean.FALSE.toString())
+                .FISCARRYRATE(Boolean.FALSE.toString())
+                .FDOCUMENTSTATUS("Z")
+                .FBUSINESSTYPE("FY")
+                .FCancelStatus("A")
+                .FREMARK(remark)
+                .FSETTLEMAINBOOKID(KingdeeNumberDTO.builder().FNUMBER(currency).build())
+                .FRECEIVEBILLENTRY(entryList)
+                .build();
+        ReceiveBillSaveDTO saveDTO = ReceiveBillSaveDTO.builder()
+                .IsDeleteEntry(Boolean.TRUE.toString())
+                .IsVerifyBaseDataField(Boolean.FALSE.toString())
+                .IsEntryBatchFill(Boolean.TRUE.toString())
+                .ValidateFlag(Boolean.TRUE.toString())
+                .NumberSearch(Boolean.TRUE.toString())
+                .IsAutoAdjustField(Boolean.FALSE.toString())
+                .InterationFlags("")
+                .IgnoreInterationFlag("")
+                .IsControlPrecision(Boolean.FALSE.toString())
+                .ValidateRepeatJson(Boolean.FALSE.toString())
+                .model(saveModelDTO)
+                .build();
+        Gson gson = new Gson();
+        String json = gson.toJson(saveDTO);
+        System.out.println("json = " + json);
+        return arReceivebillSave(getK3CloudApi(tenantId), json, recordBillNo, elderId, tenantId);
+    }
+
     @Override
     public K3CloudApi getK3CloudApi(Long tenantId) {
         SocialTypeEnum environment = SocialTypeEnum.KINGDEE_PRODUCT;

Некоторые файлы не были показаны из-за большого количества измененных файлов