mirror of
https://gitee.com/yudaocode/yudao-boot-mini.git
synced 2025-12-26 07:06:22 +08:00
Merge branch 'master' of https://gitee.com/zhijiantianya/ruoyi-vue-pro
# Conflicts: # yudao-module-ai/pom.xml # yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatMessageController.http # yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageRespVO.java # yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageSendReqVO.java # yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageSendRespVO.java # yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleRespVO.java # yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleSaveMyReqVO.java # yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleSaveReqVO.java # yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatMessageDO.java # yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiChatRoleDO.java # yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiToolDO.java # yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/enums/model/AiPlatformEnum.java # yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/AiAutoConfiguration.java # yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/YudaoAiProperties.java # yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/AiModelFactory.java # yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/AiModelFactoryImpl.java # yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/doubao/DouBaoChatModel.java # yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/hunyuan/HunYuanChatModel.java # yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/siliconflow/SiliconFlowChatModel.java # yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/xinghuo/XingHuoChatModel.java # yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/security/config/SecurityConfiguration.java # yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java # yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java # yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiModelServiceImpl.java # yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiToolServiceImpl.java # yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/function/DirectoryListToolFunction.java # yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/function/UserProfileQueryToolFunction.java # yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/function/WeatherQueryToolFunction.java # yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/util/AiUtils.java # yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/DeepSeekChatModelTests.java # yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/DouBaoChatModelTests.java # yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/HunYuanChatModelTests.java # yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/LlamaChatModelTests.java # yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/MiniMaxChatModelTests.java # yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/MoonshotChatModelTests.java # yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/OpenAIChatModelTests.java # yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/SiliconFlowChatModelTests.java # yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/TongYiChatModelTests.java # yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/XingHuoChatModelTests.java # yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/ZhiPuAiChatModelTests.java # yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/image/TongYiImagesModelTest.java # yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/mcp/DouBaoMcpTests.java # yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/api/event/BpmProcessInstanceStatusEvent.java # yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelMetaInfoVO.java # yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmTaskController.java # yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/convert/task/BpmProcessInstanceConvert.java # yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/definition/BpmProcessDefinitionInfoDO.java # yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/ErrorCodeConstants.java # yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmReasonEnum.java # yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmSequentialMultiInstanceBehavior.java # yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmnModelUtils.java # yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java # yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskService.java # yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java # yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/customer/CrmCustomerImportExcelVO.java # yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/enums/LogRecordConstants.java # yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueServiceImpl.java # yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/contact/CrmContactServiceImpl.java # yudao-module-infra/src/test/java/cn/iocoder/yudao/module/infra/framework/file/core/local/LocalFileClientTest.java # yudao-module-infra/src/test/java/cn/iocoder/yudao/module/infra/framework/file/core/s3/S3FileClientTest.java # yudao-module-infra/src/test/java/cn/iocoder/yudao/module/infra/service/file/FileConfigServiceImplTest.java # yudao-module-infra/src/test/resources/codegen/windows10/vue2_master_erp/java/InfraStudentServiceImplTest # yudao-module-infra/src/test/resources/codegen/windows10/vue2_master_inner/java/InfraStudentServiceImplTest # yudao-module-infra/src/test/resources/codegen/windows10/vue2_master_normal/java/InfraStudentServiceImplTest # yudao-module-infra/src/test/resources/codegen/windows10/vue2_one/java/InfraStudentServiceImplTest # yudao-module-infra/src/test/resources/codegen/windows10/vue2_tree/java/InfraCategoryServiceImplTest # yudao-module-infra/src/test/resources/codegen/windows10/vue3_master_erp/java/InfraStudentServiceImplTest # yudao-module-infra/src/test/resources/codegen/windows10/vue3_master_inner/java/InfraStudentServiceImplTest # yudao-module-infra/src/test/resources/codegen/windows10/vue3_master_normal/java/InfraStudentServiceImplTest # yudao-module-infra/src/test/resources/codegen/windows10/vue3_one/java/InfraStudentServiceImplTest # yudao-module-infra/src/test/resources/codegen/windows10/vue3_tree/java/InfraCategoryServiceImplTest # yudao-module-iot/pom.xml # yudao-module-iot/yudao-module-iot-biz/pom.xml # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/package-info.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceController.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDevicePropertyController.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceImportExcelVO.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDevicePageReqVO.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceRespVO.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceSaveReqVO.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/property/IotDevicePropertyHistoryListReqVO.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/property/IotDevicePropertyRespVO.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/IotOtaFirmwareController.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/firmware/IotOtaFirmwareCreateReqVO.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/firmware/IotOtaFirmwarePageReqVO.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/firmware/IotOtaFirmwareRespVO.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/firmware/IotOtaFirmwareUpdateReqVO.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/task/IotOtaTaskCreateReqVO.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/IotProductController.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductRespVO.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductSaveReqVO.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/data/sink/IotDataSinkPageReqVO.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/statistics/IotStatisticsController.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/IotThingModelController.http # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/IotThingModelController.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/vo/IotThingModelRespVO.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/vo/IotThingModelSaveReqVO.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/convert/thingmodel/IotThingModelConvert.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/alert/IotAlertConfigDO.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/alert/IotAlertRecordDO.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceDO.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/ota/IotOtaFirmwareDO.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/product/IotProductDO.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/IotDataSinkDO.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/config/IotDataSinkHttpConfig.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/config/IotDataSinkKafkaConfig.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/config/IotDataSinkMqttConfig.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/config/IotDataSinkRabbitMQConfig.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/config/IotDataSinkRocketMQConfig.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/thingmodel/IotThingModelDO.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/thingmodel/model/ThingModelEvent.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/thingmodel/model/ThingModelParam.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/thingmodel/model/ThingModelProperty.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/thingmodel/model/ThingModelService.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/thingmodel/model/dataType/ThingModelArrayDataSpecs.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/thingmodel/model/dataType/ThingModelDataSpecs.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/thingmodel/model/dataType/ThingModelDateOrTextDataSpecs.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/thingmodel/model/dataType/ThingModelNumericDataSpec.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/thingmodel/model/dataType/ThingModelStructDataSpecs.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceMapper.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/ota/IotOtaFirmwareMapper.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/thingmodel/IotThingModelMapper.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/redis/RedisKeyConstants.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/redis/device/DevicePropertyRedisDAO.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/redis/device/DeviceReportTimeRedisDAO.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/tdengine/IotDevicePropertyMapper.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/alert/IotAlertReceiveTypeEnum.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotDeviceMessageIdentifierEnum.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotDeviceMessageTypeEnum.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/ota/IotOtaTaskDeviceScopeEnum.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/ota/IotOtaTaskStatusEnum.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/product/IotLocationTypeEnum.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/product/IotNetTypeEnum.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/product/IotProductDeviceTypeEnum.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/product/IotProductStatusEnum.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotRedisDataStructureEnum.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotSceneRuleConditionOperatorEnum.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/thingmodel/IotDataSpecsDataTypeEnum.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/thingmodel/IotThingModelAccessModeEnum.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/thingmodel/IotThingModelParamDirectionEnum.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/thingmodel/IotThingModelServiceCallTypeEnum.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/thingmodel/IotThingModelServiceEventTypeEnum.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/thingmodel/IotThingModelTypeEnum.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/tdengine/config/TDengineTableInitRunner.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/tdengine/core/TDengineTableField.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/job/device/IotDeviceOfflineCheckJob.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/producer/package-info.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceService.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceServiceImpl.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/property/IotDevicePropertyServiceImpl.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaFirmwareService.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaFirmwareServiceImpl.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductCategoryServiceImpl.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductService.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductServiceImpl.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotDataRuleCacheableAction.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotHttpDataSinkAction.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotKafkaDataRuleAction.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/thingmodel/IotThingModelService.java # yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/thingmodel/IotThingModelServiceImpl.java # yudao-module-iot/yudao-module-iot-biz/src/main/resources/mapper/device/IotDevicePropertyMapper.xml # yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotDataBridgeExecuteTest.java # yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotDeviceAuthReqDTO.java # yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotDeviceStateEnum.java # yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/convert/coupon/CouponConvert.java # yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java # yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/dal/dataobject/aftersale/AfterSaleDO.java # yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/dal/dataobject/cart/CartDO.java # yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/dal/dataobject/order/TradeOrderItemDO.java # yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeStatusSyncToWxaOrderHandler.java # yudao-module-member/src/main/java/cn/iocoder/yudao/module/member/controller/app/auth/vo/AppAuthSmsSendReqVO.java # yudao-module-member/src/main/java/cn/iocoder/yudao/module/member/controller/app/auth/vo/AppAuthSmsValidateReqVO.java # yudao-module-member/src/test/java/cn/iocoder/yudao/module/member/service/auth/MemberAuthServiceTest.java # yudao-module-mp/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/vo/message/MpMessagePageReqVO.java # yudao-module-mp/src/main/java/cn/iocoder/yudao/module/mp/dal/mysql/message/MpMessageMapper.java # yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/app/order/vo/AppPayOrderSubmitReqVO.java # yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/app/order/vo/AppPayOrderSubmitRespVO.java # yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/notify/PayNotifyTaskDO.java # yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/client/dto/refund/PayRefundUnifiedReqDTO.java # yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java # yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java # yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceImpl.java # yudao-module-pay/src/test/java/cn/iocoder/yudao/module/pay/service/channel/PayChannelServiceTest.java # yudao-module-pay/src/test/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceTest.java # yudao-module-pay/src/test/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundServiceTest.java # yudao-module-system/src/test/java/cn/iocoder/yudao/module/system/service/dept/DeptServiceImplTest.java # yudao-module-system/src/test/java/cn/iocoder/yudao/module/system/service/mail/MailLogServiceImplTest.java # yudao-module-system/src/test/java/cn/iocoder/yudao/module/system/service/mail/MailSendServiceImplTest.java # yudao-module-system/src/test/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2ApproveServiceImplTest.java # yudao-module-system/src/test/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2GrantServiceImplTest.java # yudao-module-system/src/test/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2TokenServiceImplTest.java # yudao-module-system/src/test/java/cn/iocoder/yudao/module/system/service/permission/PermissionServiceTest.java # yudao-module-system/src/test/java/cn/iocoder/yudao/module/system/service/permission/RoleServiceImplTest.java # yudao-module-system/src/test/java/cn/iocoder/yudao/module/system/service/sms/SmsChannelServiceTest.java # yudao-module-system/src/test/java/cn/iocoder/yudao/module/system/service/sms/SmsLogServiceImplTest.java # yudao-module-system/src/test/java/cn/iocoder/yudao/module/system/service/sms/SmsSendServiceImplTest.java # yudao-module-system/src/test/java/cn/iocoder/yudao/module/system/service/sms/SmsTemplateServiceImplTest.java # yudao-module-system/src/test/java/cn/iocoder/yudao/module/system/service/tenant/TenantServiceImplTest.java # yudao-module-system/src/test/resources/sql/create_tables.sql
This commit is contained in:
commit
bbd78a0f66
6
pom.xml
6
pom.xml
@ -33,14 +33,14 @@
|
||||
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
|
||||
|
||||
<properties>
|
||||
<revision>2.6.1-jdk8-SNAPSHOT</revision>
|
||||
<revision>2025.08-jdk8-SNAPSHOT</revision>
|
||||
<!-- Maven 相关 -->
|
||||
<java.version>1.8</java.version>
|
||||
<maven.compiler.source>${java.version}</maven.compiler.source>
|
||||
<maven.compiler.target>${java.version}</maven.compiler.target>
|
||||
<maven-surefire-plugin.version>3.2.2</maven-surefire-plugin.version>
|
||||
<maven-surefire-plugin.version>3.5.3</maven-surefire-plugin.version>
|
||||
<maven-compiler-plugin.version>3.14.0</maven-compiler-plugin.version>
|
||||
<flatten-maven-plugin.version>1.6.0</flatten-maven-plugin.version>
|
||||
<flatten-maven-plugin.version>1.7.2</flatten-maven-plugin.version>
|
||||
<!-- maven-surefire-plugin 暂时无法通过 bom 的依赖读取(兼容老版本 IDEA 2024 及以前版本) -->
|
||||
<lombok.version>1.18.38</lombok.version>
|
||||
<spring.boot.version>2.7.18</spring.boot.version>
|
||||
|
||||
@ -4303,7 +4303,7 @@ CREATE TABLE system_tenant
|
||||
contact_name varchar(30) NOT NULL,
|
||||
contact_mobile varchar(500) DEFAULT NULL NULL,
|
||||
status smallint DEFAULT 0 NOT NULL,
|
||||
website varchar(256) DEFAULT '' NULL,
|
||||
websites varchar(256) DEFAULT '' NULL,
|
||||
package_id bigint NOT NULL,
|
||||
expire_time datetime NOT NULL,
|
||||
account_count int NOT NULL,
|
||||
@ -4320,7 +4320,7 @@ COMMENT ON COLUMN system_tenant.contact_user_id IS '联系人的用户编号';
|
||||
COMMENT ON COLUMN system_tenant.contact_name IS '联系人';
|
||||
COMMENT ON COLUMN system_tenant.contact_mobile IS '联系手机';
|
||||
COMMENT ON COLUMN system_tenant.status IS '租户状态(0正常 1停用)';
|
||||
COMMENT ON COLUMN system_tenant.website IS '绑定域名';
|
||||
COMMENT ON COLUMN system_tenant.websites IS '绑定域名数组';
|
||||
COMMENT ON COLUMN system_tenant.package_id IS '租户套餐编号';
|
||||
COMMENT ON COLUMN system_tenant.expire_time IS '过期时间';
|
||||
COMMENT ON COLUMN system_tenant.account_count IS '账号数量';
|
||||
@ -4336,9 +4336,9 @@ COMMENT ON TABLE system_tenant IS '租户表';
|
||||
-- ----------------------------
|
||||
-- @formatter:off
|
||||
SET IDENTITY_INSERT system_tenant ON;
|
||||
INSERT INTO system_tenant (id, name, contact_user_id, contact_name, contact_mobile, status, website, package_id, expire_time, account_count, creator, create_time, updater, update_time, deleted) VALUES (1, '芋道源码', NULL, '芋艿', '17321315478', 0, 'www.iocoder.cn', 0, '2099-02-19 17:14:16', 9999, '1', '2021-01-05 17:03:47', '1', '2023-11-06 11:41:41', '0');
|
||||
INSERT INTO system_tenant (id, name, contact_user_id, contact_name, contact_mobile, status, website, package_id, expire_time, account_count, creator, create_time, updater, update_time, deleted) VALUES (121, '小租户', 110, '小王2', '15601691300', 0, 'zsxq.iocoder.cn', 111, '2026-07-10 00:00:00', 30, '1', '2022-02-22 00:56:14', '1', '2025-04-03 21:33:01', '0');
|
||||
INSERT INTO system_tenant (id, name, contact_user_id, contact_name, contact_mobile, status, website, package_id, expire_time, account_count, creator, create_time, updater, update_time, deleted) VALUES (122, '测试租户', 113, '芋道', '15601691300', 0, 'test.iocoder.cn', 111, '2022-04-29 00:00:00', 50, '1', '2022-03-07 21:37:58', '1', '2024-09-22 12:10:50', '0');
|
||||
INSERT INTO system_tenant (id, name, contact_user_id, contact_name, contact_mobile, status, websites, package_id, expire_time, account_count, creator, create_time, updater, update_time, deleted) VALUES (1, '芋道源码', NULL, '芋艿', '17321315478', 0, 'www.iocoder.cn', 0, '2099-02-19 17:14:16', 9999, '1', '2021-01-05 17:03:47', '1', '2023-11-06 11:41:41', '0');
|
||||
INSERT INTO system_tenant (id, name, contact_user_id, contact_name, contact_mobile, status, websites, package_id, expire_time, account_count, creator, create_time, updater, update_time, deleted) VALUES (121, '小租户', 110, '小王2', '15601691300', 0, 'zsxq.iocoder.cn', 111, '2026-07-10 00:00:00', 30, '1', '2022-02-22 00:56:14', '1', '2025-04-03 21:33:01', '0');
|
||||
INSERT INTO system_tenant (id, name, contact_user_id, contact_name, contact_mobile, status, websites, package_id, expire_time, account_count, creator, create_time, updater, update_time, deleted) VALUES (122, '测试租户', 113, '芋道', '15601691300', 0, 'test.iocoder.cn', 111, '2022-04-29 00:00:00', 50, '1', '2022-03-07 21:37:58', '1', '2024-09-22 12:10:50', '0');
|
||||
COMMIT;
|
||||
SET IDENTITY_INSERT system_tenant OFF;
|
||||
-- @formatter:on
|
||||
|
||||
@ -4608,7 +4608,7 @@ CREATE TABLE system_tenant
|
||||
contact_name varchar(30) NOT NULL,
|
||||
contact_mobile varchar(500) NULL DEFAULT NULL,
|
||||
status int2 NOT NULL DEFAULT 0,
|
||||
website varchar(256) NULL DEFAULT '',
|
||||
websites varchar(256) NULL DEFAULT '',
|
||||
package_id int8 NOT NULL,
|
||||
expire_time timestamp NOT NULL,
|
||||
account_count int4 NOT NULL,
|
||||
@ -4628,7 +4628,7 @@ COMMENT ON COLUMN system_tenant.contact_user_id IS '联系人的用户编号';
|
||||
COMMENT ON COLUMN system_tenant.contact_name IS '联系人';
|
||||
COMMENT ON COLUMN system_tenant.contact_mobile IS '联系手机';
|
||||
COMMENT ON COLUMN system_tenant.status IS '租户状态(0正常 1停用)';
|
||||
COMMENT ON COLUMN system_tenant.website IS '绑定域名';
|
||||
COMMENT ON COLUMN system_tenant.websites IS '绑定域名数组';
|
||||
COMMENT ON COLUMN system_tenant.package_id IS '租户套餐编号';
|
||||
COMMENT ON COLUMN system_tenant.expire_time IS '过期时间';
|
||||
COMMENT ON COLUMN system_tenant.account_count IS '账号数量';
|
||||
@ -4644,9 +4644,9 @@ COMMENT ON TABLE system_tenant IS '租户表';
|
||||
-- ----------------------------
|
||||
-- @formatter:off
|
||||
BEGIN;
|
||||
INSERT INTO system_tenant (id, name, contact_user_id, contact_name, contact_mobile, status, website, package_id, expire_time, account_count, creator, create_time, updater, update_time, deleted) VALUES (1, '芋道源码', NULL, '芋艿', '17321315478', 0, 'www.iocoder.cn', 0, '2099-02-19 17:14:16', 9999, '1', '2021-01-05 17:03:47', '1', '2023-11-06 11:41:41', '0');
|
||||
INSERT INTO system_tenant (id, name, contact_user_id, contact_name, contact_mobile, status, website, package_id, expire_time, account_count, creator, create_time, updater, update_time, deleted) VALUES (121, '小租户', 110, '小王2', '15601691300', 0, 'zsxq.iocoder.cn', 111, '2026-07-10 00:00:00', 30, '1', '2022-02-22 00:56:14', '1', '2025-04-03 21:33:01', '0');
|
||||
INSERT INTO system_tenant (id, name, contact_user_id, contact_name, contact_mobile, status, website, package_id, expire_time, account_count, creator, create_time, updater, update_time, deleted) VALUES (122, '测试租户', 113, '芋道', '15601691300', 0, 'test.iocoder.cn', 111, '2022-04-29 00:00:00', 50, '1', '2022-03-07 21:37:58', '1', '2024-09-22 12:10:50', '0');
|
||||
INSERT INTO system_tenant (id, name, contact_user_id, contact_name, contact_mobile, status, websites, package_id, expire_time, account_count, creator, create_time, updater, update_time, deleted) VALUES (1, '芋道源码', NULL, '芋艿', '17321315478', 0, 'www.iocoder.cn', 0, '2099-02-19 17:14:16', 9999, '1', '2021-01-05 17:03:47', '1', '2023-11-06 11:41:41', '0');
|
||||
INSERT INTO system_tenant (id, name, contact_user_id, contact_name, contact_mobile, status, websites, package_id, expire_time, account_count, creator, create_time, updater, update_time, deleted) VALUES (121, '小租户', 110, '小王2', '15601691300', 0, 'zsxq.iocoder.cn', 111, '2026-07-10 00:00:00', 30, '1', '2022-02-22 00:56:14', '1', '2025-04-03 21:33:01', '0');
|
||||
INSERT INTO system_tenant (id, name, contact_user_id, contact_name, contact_mobile, status, websites, package_id, expire_time, account_count, creator, create_time, updater, update_time, deleted) VALUES (122, '测试租户', 113, '芋道', '15601691300', 0, 'test.iocoder.cn', 111, '2022-04-29 00:00:00', 50, '1', '2022-03-07 21:37:58', '1', '2024-09-22 12:10:50', '0');
|
||||
COMMIT;
|
||||
-- @formatter:on
|
||||
|
||||
|
||||
@ -1259,14 +1259,16 @@ CREATE TABLE `system_mail_log` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号',
|
||||
`user_id` bigint NULL DEFAULT NULL COMMENT '用户编号',
|
||||
`user_type` tinyint NULL DEFAULT NULL COMMENT '用户类型',
|
||||
`to_mail` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '接收邮箱地址',
|
||||
`to_mails` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '接收邮箱地址',
|
||||
`cc_mails` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '抄送邮箱地址',
|
||||
`bcc_mails` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '密送邮箱地址',
|
||||
`account_id` bigint NOT NULL COMMENT '邮箱账号编号',
|
||||
`from_mail` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '发送邮箱地址',
|
||||
`template_id` bigint NOT NULL COMMENT '模板编号',
|
||||
`template_code` varchar(63) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '模板编码',
|
||||
`template_nickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '模版发送人名称',
|
||||
`template_title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '邮件标题',
|
||||
`template_content` varchar(10240) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '邮件内容',
|
||||
`template_content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '邮件内容',
|
||||
`template_params` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '邮件参数',
|
||||
`send_status` tinyint NOT NULL DEFAULT 0 COMMENT '发送状态',
|
||||
`send_time` datetime NULL DEFAULT NULL COMMENT '发送时间',
|
||||
@ -3743,7 +3745,7 @@ CREATE TABLE `system_tenant` (
|
||||
`contact_name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '联系人',
|
||||
`contact_mobile` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '联系手机',
|
||||
`status` tinyint NOT NULL DEFAULT 0 COMMENT '租户状态(0正常 1停用)',
|
||||
`website` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '绑定域名',
|
||||
`websites` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '绑定域名数组',
|
||||
`package_id` bigint NOT NULL COMMENT '租户套餐编号',
|
||||
`expire_time` datetime NOT NULL COMMENT '过期时间',
|
||||
`account_count` int NOT NULL COMMENT '账号数量',
|
||||
@ -3759,9 +3761,9 @@ CREATE TABLE `system_tenant` (
|
||||
-- Records of system_tenant
|
||||
-- ----------------------------
|
||||
BEGIN;
|
||||
INSERT INTO `system_tenant` (`id`, `name`, `contact_user_id`, `contact_name`, `contact_mobile`, `status`, `website`, `package_id`, `expire_time`, `account_count`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1, '芋道源码', NULL, '芋艿', '17321315478', 0, 'www.iocoder.cn', 0, '2099-02-19 17:14:16', 9999, '1', '2021-01-05 17:03:47', '1', '2023-11-06 11:41:41', b'0');
|
||||
INSERT INTO `system_tenant` (`id`, `name`, `contact_user_id`, `contact_name`, `contact_mobile`, `status`, `website`, `package_id`, `expire_time`, `account_count`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (121, '小租户', 110, '小王2', '15601691300', 0, 'zsxq.iocoder.cn', 111, '2026-07-10 00:00:00', 30, '1', '2022-02-22 00:56:14', '1', '2025-04-03 21:33:01', b'0');
|
||||
INSERT INTO `system_tenant` (`id`, `name`, `contact_user_id`, `contact_name`, `contact_mobile`, `status`, `website`, `package_id`, `expire_time`, `account_count`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (122, '测试租户', 113, '芋道', '15601691300', 0, 'test.iocoder.cn', 111, '2022-04-29 00:00:00', 50, '1', '2022-03-07 21:37:58', '1', '2024-09-22 12:10:50', b'0');
|
||||
INSERT INTO `system_tenant` (`id`, `name`, `contact_user_id`, `contact_name`, `contact_mobile`, `status`, `websites`, `package_id`, `expire_time`, `account_count`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1, '芋道源码', NULL, '芋艿', '17321315478', 0, 'www.iocoder.cn', 0, '2099-02-19 17:14:16', 9999, '1', '2021-01-05 17:03:47', '1', '2023-11-06 11:41:41', b'0');
|
||||
INSERT INTO `system_tenant` (`id`, `name`, `contact_user_id`, `contact_name`, `contact_mobile`, `status`, `websites`, `package_id`, `expire_time`, `account_count`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (121, '小租户', 110, '小王2', '15601691300', 0, 'zsxq.iocoder.cn', 111, '2026-07-10 00:00:00', 30, '1', '2022-02-22 00:56:14', '1', '2025-04-03 21:33:01', b'0');
|
||||
INSERT INTO `system_tenant` (`id`, `name`, `contact_user_id`, `contact_name`, `contact_mobile`, `status`, `websites`, `package_id`, `expire_time`, `account_count`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (122, '测试租户', 113, '芋道', '15601691300', 0, 'test.iocoder.cn', 111, '2022-04-29 00:00:00', 50, '1', '2022-03-07 21:37:58', '1', '2024-09-22 12:10:50', b'0');
|
||||
COMMIT;
|
||||
|
||||
-- ----------------------------
|
||||
|
||||
@ -4608,7 +4608,7 @@ CREATE TABLE system_tenant
|
||||
contact_name varchar(30) NOT NULL,
|
||||
contact_mobile varchar(500) NULL DEFAULT NULL,
|
||||
status int2 NOT NULL DEFAULT 0,
|
||||
website varchar(256) NULL DEFAULT '',
|
||||
websites varchar(256) NULL DEFAULT '',
|
||||
package_id int8 NOT NULL,
|
||||
expire_time timestamp NOT NULL,
|
||||
account_count int4 NOT NULL,
|
||||
@ -4628,7 +4628,7 @@ COMMENT ON COLUMN system_tenant.contact_user_id IS '联系人的用户编号';
|
||||
COMMENT ON COLUMN system_tenant.contact_name IS '联系人';
|
||||
COMMENT ON COLUMN system_tenant.contact_mobile IS '联系手机';
|
||||
COMMENT ON COLUMN system_tenant.status IS '租户状态(0正常 1停用)';
|
||||
COMMENT ON COLUMN system_tenant.website IS '绑定域名';
|
||||
COMMENT ON COLUMN system_tenant.websites IS '绑定域名数组';
|
||||
COMMENT ON COLUMN system_tenant.package_id IS '租户套餐编号';
|
||||
COMMENT ON COLUMN system_tenant.expire_time IS '过期时间';
|
||||
COMMENT ON COLUMN system_tenant.account_count IS '账号数量';
|
||||
@ -4644,9 +4644,9 @@ COMMENT ON TABLE system_tenant IS '租户表';
|
||||
-- ----------------------------
|
||||
-- @formatter:off
|
||||
BEGIN;
|
||||
INSERT INTO system_tenant (id, name, contact_user_id, contact_name, contact_mobile, status, website, package_id, expire_time, account_count, creator, create_time, updater, update_time, deleted) VALUES (1, '芋道源码', NULL, '芋艿', '17321315478', 0, 'www.iocoder.cn', 0, '2099-02-19 17:14:16', 9999, '1', '2021-01-05 17:03:47', '1', '2023-11-06 11:41:41', '0');
|
||||
INSERT INTO system_tenant (id, name, contact_user_id, contact_name, contact_mobile, status, website, package_id, expire_time, account_count, creator, create_time, updater, update_time, deleted) VALUES (121, '小租户', 110, '小王2', '15601691300', 0, 'zsxq.iocoder.cn', 111, '2026-07-10 00:00:00', 30, '1', '2022-02-22 00:56:14', '1', '2025-04-03 21:33:01', '0');
|
||||
INSERT INTO system_tenant (id, name, contact_user_id, contact_name, contact_mobile, status, website, package_id, expire_time, account_count, creator, create_time, updater, update_time, deleted) VALUES (122, '测试租户', 113, '芋道', '15601691300', 0, 'test.iocoder.cn', 111, '2022-04-29 00:00:00', 50, '1', '2022-03-07 21:37:58', '1', '2024-09-22 12:10:50', '0');
|
||||
INSERT INTO system_tenant (id, name, contact_user_id, contact_name, contact_mobile, status, websites, package_id, expire_time, account_count, creator, create_time, updater, update_time, deleted) VALUES (1, '芋道源码', NULL, '芋艿', '17321315478', 0, 'www.iocoder.cn', 0, '2099-02-19 17:14:16', 9999, '1', '2021-01-05 17:03:47', '1', '2023-11-06 11:41:41', '0');
|
||||
INSERT INTO system_tenant (id, name, contact_user_id, contact_name, contact_mobile, status, websites, package_id, expire_time, account_count, creator, create_time, updater, update_time, deleted) VALUES (121, '小租户', 110, '小王2', '15601691300', 0, 'zsxq.iocoder.cn', 111, '2026-07-10 00:00:00', 30, '1', '2022-02-22 00:56:14', '1', '2025-04-03 21:33:01', '0');
|
||||
INSERT INTO system_tenant (id, name, contact_user_id, contact_name, contact_mobile, status, websites, package_id, expire_time, account_count, creator, create_time, updater, update_time, deleted) VALUES (122, '测试租户', 113, '芋道', '15601691300', 0, 'test.iocoder.cn', 111, '2022-04-29 00:00:00', 50, '1', '2022-03-07 21:37:58', '1', '2024-09-22 12:10:50', '0');
|
||||
COMMIT;
|
||||
-- @formatter:on
|
||||
|
||||
|
||||
@ -4495,7 +4495,7 @@ CREATE TABLE system_tenant
|
||||
contact_name varchar2(30) NULL,
|
||||
contact_mobile varchar2(500) DEFAULT NULL NULL,
|
||||
status smallint DEFAULT 0 NOT NULL,
|
||||
website varchar2(256) DEFAULT '' NULL,
|
||||
websites varchar2(256) DEFAULT '' NULL,
|
||||
package_id number NOT NULL,
|
||||
expire_time date NOT NULL,
|
||||
account_count number NOT NULL,
|
||||
@ -4515,7 +4515,7 @@ COMMENT ON COLUMN system_tenant.contact_user_id IS '联系人的用户编号';
|
||||
COMMENT ON COLUMN system_tenant.contact_name IS '联系人';
|
||||
COMMENT ON COLUMN system_tenant.contact_mobile IS '联系手机';
|
||||
COMMENT ON COLUMN system_tenant.status IS '租户状态(0正常 1停用)';
|
||||
COMMENT ON COLUMN system_tenant.website IS '绑定域名';
|
||||
COMMENT ON COLUMN system_tenant.websites IS '绑定域名数组';
|
||||
COMMENT ON COLUMN system_tenant.package_id IS '租户套餐编号';
|
||||
COMMENT ON COLUMN system_tenant.expire_time IS '过期时间';
|
||||
COMMENT ON COLUMN system_tenant.account_count IS '账号数量';
|
||||
@ -4530,9 +4530,9 @@ COMMENT ON TABLE system_tenant IS '租户表';
|
||||
-- Records of system_tenant
|
||||
-- ----------------------------
|
||||
-- @formatter:off
|
||||
INSERT INTO system_tenant (id, name, contact_user_id, contact_name, contact_mobile, status, website, package_id, expire_time, account_count, creator, create_time, updater, update_time, deleted) VALUES (1, '芋道源码', NULL, '芋艿', '17321315478', 0, 'www.iocoder.cn', 0, to_date('2099-02-19 17:14:16', 'SYYYY-MM-DD HH24:MI:SS'), 9999, '1', to_date('2021-01-05 17:03:47', 'SYYYY-MM-DD HH24:MI:SS'), '1', to_date('2023-11-06 11:41:41', 'SYYYY-MM-DD HH24:MI:SS'), '0');
|
||||
INSERT INTO system_tenant (id, name, contact_user_id, contact_name, contact_mobile, status, website, package_id, expire_time, account_count, creator, create_time, updater, update_time, deleted) VALUES (121, '小租户', 110, '小王2', '15601691300', 0, 'zsxq.iocoder.cn', 111, to_date('2026-07-10 00:00:00', 'SYYYY-MM-DD HH24:MI:SS'), 30, '1', to_date('2022-02-22 00:56:14', 'SYYYY-MM-DD HH24:MI:SS'), '1', to_date('2025-04-03 21:33:01', 'SYYYY-MM-DD HH24:MI:SS'), '0');
|
||||
INSERT INTO system_tenant (id, name, contact_user_id, contact_name, contact_mobile, status, website, package_id, expire_time, account_count, creator, create_time, updater, update_time, deleted) VALUES (122, '测试租户', 113, '芋道', '15601691300', 0, 'test.iocoder.cn', 111, to_date('2022-04-29 00:00:00', 'SYYYY-MM-DD HH24:MI:SS'), 50, '1', to_date('2022-03-07 21:37:58', 'SYYYY-MM-DD HH24:MI:SS'), '1', to_date('2024-09-22 12:10:50', 'SYYYY-MM-DD HH24:MI:SS'), '0');
|
||||
INSERT INTO system_tenant (id, name, contact_user_id, contact_name, contact_mobile, status, websites, package_id, expire_time, account_count, creator, create_time, updater, update_time, deleted) VALUES (1, '芋道源码', NULL, '芋艿', '17321315478', 0, 'www.iocoder.cn', 0, to_date('2099-02-19 17:14:16', 'SYYYY-MM-DD HH24:MI:SS'), 9999, '1', to_date('2021-01-05 17:03:47', 'SYYYY-MM-DD HH24:MI:SS'), '1', to_date('2023-11-06 11:41:41', 'SYYYY-MM-DD HH24:MI:SS'), '0');
|
||||
INSERT INTO system_tenant (id, name, contact_user_id, contact_name, contact_mobile, status, websites, package_id, expire_time, account_count, creator, create_time, updater, update_time, deleted) VALUES (121, '小租户', 110, '小王2', '15601691300', 0, 'zsxq.iocoder.cn', 111, to_date('2026-07-10 00:00:00', 'SYYYY-MM-DD HH24:MI:SS'), 30, '1', to_date('2022-02-22 00:56:14', 'SYYYY-MM-DD HH24:MI:SS'), '1', to_date('2025-04-03 21:33:01', 'SYYYY-MM-DD HH24:MI:SS'), '0');
|
||||
INSERT INTO system_tenant (id, name, contact_user_id, contact_name, contact_mobile, status, websites, package_id, expire_time, account_count, creator, create_time, updater, update_time, deleted) VALUES (122, '测试租户', 113, '芋道', '15601691300', 0, 'test.iocoder.cn', 111, to_date('2022-04-29 00:00:00', 'SYYYY-MM-DD HH24:MI:SS'), 50, '1', to_date('2022-03-07 21:37:58', 'SYYYY-MM-DD HH24:MI:SS'), '1', to_date('2024-09-22 12:10:50', 'SYYYY-MM-DD HH24:MI:SS'), '0');
|
||||
COMMIT;
|
||||
-- @formatter:on
|
||||
|
||||
|
||||
@ -4608,7 +4608,7 @@ CREATE TABLE system_tenant
|
||||
contact_name varchar(30) NOT NULL,
|
||||
contact_mobile varchar(500) NULL DEFAULT NULL,
|
||||
status int2 NOT NULL DEFAULT 0,
|
||||
website varchar(256) NULL DEFAULT '',
|
||||
websites varchar(256) NULL DEFAULT '',
|
||||
package_id int8 NOT NULL,
|
||||
expire_time timestamp NOT NULL,
|
||||
account_count int4 NOT NULL,
|
||||
@ -4628,7 +4628,7 @@ COMMENT ON COLUMN system_tenant.contact_user_id IS '联系人的用户编号';
|
||||
COMMENT ON COLUMN system_tenant.contact_name IS '联系人';
|
||||
COMMENT ON COLUMN system_tenant.contact_mobile IS '联系手机';
|
||||
COMMENT ON COLUMN system_tenant.status IS '租户状态(0正常 1停用)';
|
||||
COMMENT ON COLUMN system_tenant.website IS '绑定域名';
|
||||
COMMENT ON COLUMN system_tenant.websites IS '绑定域名数组';
|
||||
COMMENT ON COLUMN system_tenant.package_id IS '租户套餐编号';
|
||||
COMMENT ON COLUMN system_tenant.expire_time IS '过期时间';
|
||||
COMMENT ON COLUMN system_tenant.account_count IS '账号数量';
|
||||
@ -4644,9 +4644,9 @@ COMMENT ON TABLE system_tenant IS '租户表';
|
||||
-- ----------------------------
|
||||
-- @formatter:off
|
||||
BEGIN;
|
||||
INSERT INTO system_tenant (id, name, contact_user_id, contact_name, contact_mobile, status, website, package_id, expire_time, account_count, creator, create_time, updater, update_time, deleted) VALUES (1, '芋道源码', NULL, '芋艿', '17321315478', 0, 'www.iocoder.cn', 0, '2099-02-19 17:14:16', 9999, '1', '2021-01-05 17:03:47', '1', '2023-11-06 11:41:41', '0');
|
||||
INSERT INTO system_tenant (id, name, contact_user_id, contact_name, contact_mobile, status, website, package_id, expire_time, account_count, creator, create_time, updater, update_time, deleted) VALUES (121, '小租户', 110, '小王2', '15601691300', 0, 'zsxq.iocoder.cn', 111, '2026-07-10 00:00:00', 30, '1', '2022-02-22 00:56:14', '1', '2025-04-03 21:33:01', '0');
|
||||
INSERT INTO system_tenant (id, name, contact_user_id, contact_name, contact_mobile, status, website, package_id, expire_time, account_count, creator, create_time, updater, update_time, deleted) VALUES (122, '测试租户', 113, '芋道', '15601691300', 0, 'test.iocoder.cn', 111, '2022-04-29 00:00:00', 50, '1', '2022-03-07 21:37:58', '1', '2024-09-22 12:10:50', '0');
|
||||
INSERT INTO system_tenant (id, name, contact_user_id, contact_name, contact_mobile, status, websites, package_id, expire_time, account_count, creator, create_time, updater, update_time, deleted) VALUES (1, '芋道源码', NULL, '芋艿', '17321315478', 0, 'www.iocoder.cn', 0, '2099-02-19 17:14:16', 9999, '1', '2021-01-05 17:03:47', '1', '2023-11-06 11:41:41', '0');
|
||||
INSERT INTO system_tenant (id, name, contact_user_id, contact_name, contact_mobile, status, websites, package_id, expire_time, account_count, creator, create_time, updater, update_time, deleted) VALUES (121, '小租户', 110, '小王2', '15601691300', 0, 'zsxq.iocoder.cn', 111, '2026-07-10 00:00:00', 30, '1', '2022-02-22 00:56:14', '1', '2025-04-03 21:33:01', '0');
|
||||
INSERT INTO system_tenant (id, name, contact_user_id, contact_name, contact_mobile, status, websites, package_id, expire_time, account_count, creator, create_time, updater, update_time, deleted) VALUES (122, '测试租户', 113, '芋道', '15601691300', 0, 'test.iocoder.cn', 111, '2022-04-29 00:00:00', 50, '1', '2022-03-07 21:37:58', '1', '2024-09-22 12:10:50', '0');
|
||||
COMMIT;
|
||||
-- @formatter:on
|
||||
|
||||
|
||||
@ -10834,7 +10834,7 @@ CREATE TABLE system_tenant
|
||||
contact_name nvarchar(30) NOT NULL,
|
||||
contact_mobile nvarchar(500) DEFAULT NULL NULL,
|
||||
status tinyint DEFAULT 0 NOT NULL,
|
||||
website nvarchar(256) DEFAULT '' NULL,
|
||||
websites nvarchar(256) DEFAULT '' NULL,
|
||||
package_id bigint NOT NULL,
|
||||
expire_time datetime2 NOT NULL,
|
||||
account_count int NOT NULL,
|
||||
@ -10965,11 +10965,11 @@ BEGIN TRANSACTION
|
||||
GO
|
||||
SET IDENTITY_INSERT system_tenant ON
|
||||
GO
|
||||
INSERT INTO system_tenant (id, name, contact_user_id, contact_name, contact_mobile, status, website, package_id, expire_time, account_count, creator, create_time, updater, update_time, deleted) VALUES (1, N'芋道源码', NULL, N'芋艿', N'17321315478', 0, N'www.iocoder.cn', 0, N'2099-02-19 17:14:16', 9999, N'1', N'2021-01-05 17:03:47', N'1', N'2023-11-06 11:41:41', N'0')
|
||||
INSERT INTO system_tenant (id, name, contact_user_id, contact_name, contact_mobile, status, websites, package_id, expire_time, account_count, creator, create_time, updater, update_time, deleted) VALUES (1, N'芋道源码', NULL, N'芋艿', N'17321315478', 0, N'www.iocoder.cn', 0, N'2099-02-19 17:14:16', 9999, N'1', N'2021-01-05 17:03:47', N'1', N'2023-11-06 11:41:41', N'0')
|
||||
GO
|
||||
INSERT INTO system_tenant (id, name, contact_user_id, contact_name, contact_mobile, status, website, package_id, expire_time, account_count, creator, create_time, updater, update_time, deleted) VALUES (121, N'小租户', 110, N'小王2', N'15601691300', 0, N'zsxq.iocoder.cn', 111, N'2026-07-10 00:00:00', 30, N'1', N'2022-02-22 00:56:14', N'1', N'2025-04-03 21:33:01', N'0')
|
||||
INSERT INTO system_tenant (id, name, contact_user_id, contact_name, contact_mobile, status, websites, package_id, expire_time, account_count, creator, create_time, updater, update_time, deleted) VALUES (121, N'小租户', 110, N'小王2', N'15601691300', 0, N'zsxq.iocoder.cn', 111, N'2026-07-10 00:00:00', 30, N'1', N'2022-02-22 00:56:14', N'1', N'2025-04-03 21:33:01', N'0')
|
||||
GO
|
||||
INSERT INTO system_tenant (id, name, contact_user_id, contact_name, contact_mobile, status, website, package_id, expire_time, account_count, creator, create_time, updater, update_time, deleted) VALUES (122, N'测试租户', 113, N'芋道', N'15601691300', 0, N'test.iocoder.cn', 111, N'2022-04-29 00:00:00', 50, N'1', N'2022-03-07 21:37:58', N'1', N'2024-09-22 12:10:50', N'0')
|
||||
INSERT INTO system_tenant (id, name, contact_user_id, contact_name, contact_mobile, status, websites, package_id, expire_time, account_count, creator, create_time, updater, update_time, deleted) VALUES (122, N'测试租户', 113, N'芋道', N'15601691300', 0, N'test.iocoder.cn', 111, N'2022-04-29 00:00:00', 50, N'1', N'2022-03-07 21:37:58', N'1', N'2024-09-22 12:10:50', N'0')
|
||||
GO
|
||||
SET IDENTITY_INSERT system_tenant OFF
|
||||
GO
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
|
||||
|
||||
<properties>
|
||||
<revision>2.6.1-jdk8-SNAPSHOT</revision>
|
||||
<revision>2025.08-jdk8-SNAPSHOT</revision>
|
||||
<flatten-maven-plugin.version>1.6.0</flatten-maven-plugin.version>
|
||||
<!-- 统一依赖管理 -->
|
||||
<spring.framework.version>5.3.39</spring.framework.version>
|
||||
@ -25,19 +25,19 @@
|
||||
<knife4j.version>4.5.0</knife4j.version>
|
||||
<servlet.versoin>2.5</servlet.versoin>
|
||||
<!-- DB 相关 -->
|
||||
<druid.version>1.2.24</druid.version>
|
||||
<druid.version>1.2.27</druid.version>
|
||||
<mybatis.version>3.5.19</mybatis.version>
|
||||
<mybatis-plus.version>3.5.12</mybatis-plus.version>
|
||||
<mybatis-plus-join.version>1.5.4</mybatis-plus-join.version>
|
||||
<dynamic-datasource.version>4.3.1</dynamic-datasource.version>
|
||||
<easy-trans.version>3.0.6</easy-trans.version>
|
||||
<redisson.version>3.41.0</redisson.version>
|
||||
<redisson.version>3.51.0</redisson.version>
|
||||
<dm8.jdbc.version>8.1.3.140</dm8.jdbc.version>
|
||||
<kingbase.jdbc.version>8.6.0</kingbase.jdbc.version>
|
||||
<opengauss.jdbc.version>5.1.0</opengauss.jdbc.version>
|
||||
<taos.version>3.3.3</taos.version>
|
||||
<taos.version>3.7.3</taos.version>
|
||||
<!-- 消息队列 -->
|
||||
<rocketmq-spring.version>2.3.2</rocketmq-spring.version>
|
||||
<rocketmq-spring.version>2.3.4</rocketmq-spring.version>
|
||||
<!-- 服务保障相关 -->
|
||||
<lock4j.version>2.2.7</lock4j.version>
|
||||
<!-- 监控相关 -->
|
||||
@ -46,27 +46,28 @@
|
||||
<opentracing.version>0.33.0</opentracing.version>
|
||||
<!-- Test 测试相关 -->
|
||||
<podam.version>7.2.11.RELEASE</podam.version> <!-- Spring Boot 2.X 最多使用 7.2.11 版本 -->
|
||||
<jedis-mock.version>1.1.8</jedis-mock.version>
|
||||
<jedis-mock.version>1.1.11</jedis-mock.version>
|
||||
<mockito-inline.version>4.11.0</mockito-inline.version>
|
||||
<!-- Bpm 工作流相关 -->
|
||||
<flowable.version>6.8.0</flowable.version>
|
||||
<!-- 工具类相关 -->
|
||||
<anji-plus-captcha.version>1.4.0</anji-plus-captcha.version>
|
||||
<jsoup.version>1.18.3</jsoup.version>
|
||||
<jsoup.version>1.21.2</jsoup.version>
|
||||
<lombok.version>1.18.38</lombok.version>
|
||||
<mapstruct.version>1.6.3</mapstruct.version>
|
||||
<hutool.version>5.8.35</hutool.version>
|
||||
<fastexcel.version>1.2.0</fastexcel.version>
|
||||
<hutool-5.version>5.8.40</hutool-5.version>
|
||||
<fastexcel.version>1.3.0</fastexcel.version>
|
||||
<velocity.version>2.4</velocity.version> <!-- JDK8 不能从 2.4 升级到 2.4.1,会报包不存在!!!! -->
|
||||
<fastjson.version>1.2.83</fastjson.version>
|
||||
<guava.version>33.4.8-jre</guava.version>
|
||||
<transmittable-thread-local.version>2.14.5</transmittable-thread-local.version>
|
||||
<commons-net.version>3.11.1</commons-net.version>
|
||||
<commons-lang3.version>3.18.0</commons-lang3.version>
|
||||
<jsch.version>0.1.55</jsch.version>
|
||||
<tika-core.version>2.9.3</tika-core.version> <!-- JDK8 不能从 2.9.3 升级到 3.X,会报 JDK8 不支持 -->
|
||||
<ip2region.version>2.7.0</ip2region.version>
|
||||
<bizlog-sdk.version>3.0.6</bizlog-sdk.version>
|
||||
<netty.version>4.1.118.Final</netty.version>
|
||||
<netty.version>4.2.4.Final</netty.version>
|
||||
<mqtt.version>1.2.5</mqtt.version>
|
||||
<pf4j-spring.version>0.9.0</pf4j-spring.version>
|
||||
<vertx.version>4.5.13</vertx.version>
|
||||
@ -74,9 +75,9 @@
|
||||
<awssdk.version>2.30.14</awssdk.version>
|
||||
<justauth.version>1.16.7</justauth.version>
|
||||
<justauth-starter.version>1.4.0</justauth-starter.version>
|
||||
<jimureport.version>2.1.0</jimureport.version>
|
||||
<jimubi.version>1.9.5</jimubi.version>
|
||||
<weixin-java.version>4.7.5.B</weixin-java.version>
|
||||
<jimureport.version>2.1.1</jimureport.version>
|
||||
<jimubi.version>2.1.0</jimubi.version>
|
||||
<weixin-java.version>4.7.7-20250808.182223</weixin-java.version>
|
||||
<!-- 专属于 JDK8 安全漏洞升级 -->
|
||||
<logback.version>1.2.13</logback.version> <!-- 无法使用 1.3.X 版本,启动会报错 -->
|
||||
</properties>
|
||||
@ -267,7 +268,7 @@
|
||||
<exclusion>
|
||||
<groupId>org.redisson</groupId>
|
||||
<!-- 使用 redisson-spring-data-27 替代,解决 Tuple NoClassDefFoundError 报错 -->
|
||||
<artifactId>redisson-spring-data-34</artifactId>
|
||||
<artifactId>redisson-spring-data-35</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
@ -489,7 +490,7 @@
|
||||
<dependency>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-all</artifactId>
|
||||
<version>${hutool.version}</version>
|
||||
<version>${hutool-5.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@ -533,13 +534,18 @@
|
||||
<artifactId>commons-net</artifactId> <!-- 解决 ftp 连接 -->
|
||||
<version>${commons-net.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.jcraft</groupId>
|
||||
<artifactId>jsch</artifactId> <!-- 解决 sftp 连接 -->
|
||||
<version>${jsch.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
<version>${commons-lang3.version}</version> <!-- 解决 CVE-2025-48924 漏洞 -->
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.anji-plus</groupId>
|
||||
<artifactId>captcha-spring-boot-starter</artifactId> <!-- 验证码,一般用于登录使用 -->
|
||||
@ -614,6 +620,10 @@
|
||||
<groupId>com.github.jsqlparser</groupId>
|
||||
<artifactId>jsqlparser</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-core</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
package cn.iocoder.yudao.framework.common.biz.system.oauth2.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
@ -12,7 +11,6 @@ import java.time.LocalDateTime;
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public class OAuth2AccessTokenRespDTO implements Serializable {
|
||||
|
||||
/**
|
||||
|
||||
@ -16,6 +16,7 @@ import java.util.Arrays;
|
||||
@AllArgsConstructor
|
||||
public enum DateIntervalEnum implements ArrayValuable<Integer> {
|
||||
|
||||
HOUR(0, "小时"), // 特殊:字典里,暂时不会有这个枚举!!!因为大多数情况下,用不到这个间隔
|
||||
DAY(1, "天"),
|
||||
WEEK(2, "周"),
|
||||
MONTH(3, "月"),
|
||||
|
||||
@ -15,6 +15,8 @@ public interface WebFilterOrderEnum {
|
||||
|
||||
int REQUEST_BODY_CACHE_FILTER = Integer.MIN_VALUE + 500;
|
||||
|
||||
int API_ENCRYPT_FILTER = REQUEST_BODY_CACHE_FILTER + 1;
|
||||
|
||||
// OrderedRequestContextFilter 默认为 -105,用于国际化上下文等等
|
||||
|
||||
int TENANT_CONTEXT_FILTER = - 104; // 需要保证在 ApiAccessLogFilter 前面
|
||||
|
||||
@ -25,16 +25,16 @@ public class CommonResult<T> implements Serializable {
|
||||
* @see ErrorCode#getCode()
|
||||
*/
|
||||
private Integer code;
|
||||
/**
|
||||
* 返回数据
|
||||
*/
|
||||
private T data;
|
||||
/**
|
||||
* 错误提示,用户可阅读
|
||||
*
|
||||
* @see ErrorCode#getMsg() ()
|
||||
*/
|
||||
private String msg;
|
||||
/**
|
||||
* 返回数据
|
||||
*/
|
||||
private T data;
|
||||
|
||||
/**
|
||||
* 将传入的 result 对象,转换成另外一个泛型结果的对象
|
||||
|
||||
@ -11,12 +11,12 @@ import java.util.List;
|
||||
@Data
|
||||
public final class PageResult<T> implements Serializable {
|
||||
|
||||
@Schema(description = "数据", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private List<T> list;
|
||||
|
||||
@Schema(description = "总量", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private Long total;
|
||||
|
||||
@Schema(description = "数据", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private List<T> list;
|
||||
|
||||
public PageResult() {
|
||||
}
|
||||
|
||||
|
||||
@ -8,6 +8,7 @@ import cn.hutool.core.lang.Assert;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.iocoder.yudao.framework.common.enums.DateIntervalEnum;
|
||||
|
||||
import java.sql.Timestamp;
|
||||
import java.time.*;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.DateTimeParseException;
|
||||
@ -16,8 +17,7 @@ import java.time.temporal.TemporalAdjusters;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static cn.hutool.core.date.DatePattern.UTC_MS_WITH_XXX_OFFSET_PATTERN;
|
||||
import static cn.hutool.core.date.DatePattern.createFormatter;
|
||||
import static cn.hutool.core.date.DatePattern.*;
|
||||
|
||||
/**
|
||||
* 时间工具类,用于 {@link LocalDateTime}
|
||||
@ -82,6 +82,21 @@ public class LocalDateTimeUtils {
|
||||
return new LocalDateTime[]{buildTime(year1, month1, day1), buildTime(year2, month2, day2)};
|
||||
}
|
||||
|
||||
/**
|
||||
* 判指定断时间,是否在该时间范围内
|
||||
*
|
||||
* @param startTime 开始时间
|
||||
* @param endTime 结束时间
|
||||
* @param time 指定时间
|
||||
* @return 是否
|
||||
*/
|
||||
public static boolean isBetween(LocalDateTime startTime, LocalDateTime endTime, Timestamp time) {
|
||||
if (startTime == null || endTime == null || time == null) {
|
||||
return false;
|
||||
}
|
||||
return LocalDateTimeUtil.isIn(LocalDateTimeUtil.of(time), startTime, endTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判指定断时间,是否在该时间范围内
|
||||
*
|
||||
@ -234,6 +249,11 @@ public class LocalDateTimeUtils {
|
||||
// 2. 循环,生成时间范围
|
||||
List<LocalDateTime[]> timeRanges = new ArrayList<>();
|
||||
switch (intervalEnum) {
|
||||
case HOUR:
|
||||
while (startTime.isBefore(endTime)) {
|
||||
timeRanges.add(new LocalDateTime[]{startTime, startTime.plusHours(1).minusNanos(1)});
|
||||
startTime = startTime.plusHours(1);
|
||||
}
|
||||
case DAY:
|
||||
while (startTime.isBefore(endTime)) {
|
||||
timeRanges.add(new LocalDateTime[]{startTime, startTime.plusDays(1).minusNanos(1)});
|
||||
@ -297,6 +317,8 @@ public class LocalDateTimeUtils {
|
||||
|
||||
// 2. 循环,生成时间范围
|
||||
switch (intervalEnum) {
|
||||
case HOUR:
|
||||
return LocalDateTimeUtil.format(startTime, DatePattern.NORM_DATETIME_MINUTE_PATTERN);
|
||||
case DAY:
|
||||
return LocalDateTimeUtil.format(startTime, DatePattern.NORM_DATE_PATTERN);
|
||||
case WEEK:
|
||||
|
||||
@ -49,8 +49,15 @@ public class HttpUtils {
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private String append(String base, Map<String, ?> query, boolean fragment) {
|
||||
return append(base, query, null, fragment);
|
||||
public static String removeUrlQuery(String url) {
|
||||
if (!StrUtil.contains(url, '?')) {
|
||||
return url;
|
||||
}
|
||||
UrlBuilder builder = UrlBuilder.of(url, Charset.defaultCharset());
|
||||
// 移除 query、fragment
|
||||
builder.setQuery(null);
|
||||
builder.setFragment(null);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -3,18 +3,23 @@ package cn.iocoder.yudao.framework.common.util.json;
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import cn.iocoder.yudao.framework.common.util.json.databind.TimestampLocalDateTimeDeserializer;
|
||||
import cn.iocoder.yudao.framework.common.util.json.databind.TimestampLocalDateTimeSerializer;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
import lombok.Getter;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Type;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@ -26,13 +31,18 @@ import java.util.List;
|
||||
@Slf4j
|
||||
public class JsonUtils {
|
||||
|
||||
@Getter
|
||||
private static ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
static {
|
||||
objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
|
||||
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); // 忽略 null 值
|
||||
objectMapper.registerModules(new JavaTimeModule()); // 解决 LocalDateTime 的序列化
|
||||
// 解决 LocalDateTime 的序列化
|
||||
SimpleModule simpleModule = new JavaTimeModule()
|
||||
.addSerializer(LocalDateTime.class, TimestampLocalDateTimeSerializer.INSTANCE)
|
||||
.addDeserializer(LocalDateTime.class, TimestampLocalDateTimeDeserializer.INSTANCE);
|
||||
objectMapper.registerModules(simpleModule);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -99,6 +109,18 @@ public class JsonUtils {
|
||||
}
|
||||
}
|
||||
|
||||
public static <T> T parseObject(byte[] text, Type type) {
|
||||
if (ArrayUtil.isEmpty(text)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return objectMapper.readValue(text, objectMapper.getTypeFactory().constructType(type));
|
||||
} catch (IOException e) {
|
||||
log.error("json parse err,json:{}", text, e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将字符串解析成指定类型的对象
|
||||
* 使用 {@link #parseObject(String, Class)} 时,在@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) 的场景下,
|
||||
|
||||
@ -60,4 +60,8 @@ public class ObjectUtils {
|
||||
return Arrays.asList(array).contains(obj);
|
||||
}
|
||||
|
||||
public static boolean isNotAllEmpty(Object... objs) {
|
||||
return !ObjectUtil.isAllEmpty(objs);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -44,8 +44,10 @@ import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandl
|
||||
import org.springframework.web.util.pattern.PathPattern;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
|
||||
|
||||
@ -84,41 +86,13 @@ public class YudaoTenantAutoConfiguration {
|
||||
// ========== WEB ==========
|
||||
|
||||
@Bean
|
||||
public FilterRegistrationBean<TenantContextWebFilter> tenantContextWebFilter(TenantProperties tenantProperties) {
|
||||
public FilterRegistrationBean<TenantContextWebFilter> tenantContextWebFilter() {
|
||||
FilterRegistrationBean<TenantContextWebFilter> registrationBean = new FilterRegistrationBean<>();
|
||||
registrationBean.setFilter(new TenantContextWebFilter());
|
||||
registrationBean.setOrder(WebFilterOrderEnum.TENANT_CONTEXT_FILTER);
|
||||
addIgnoreUrls(tenantProperties);
|
||||
return registrationBean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 如果 Controller 接口上,有 {@link TenantIgnore} 注解,那么添加到忽略的 URL 中
|
||||
*
|
||||
* @param tenantProperties 租户配置
|
||||
*/
|
||||
private void addIgnoreUrls(TenantProperties tenantProperties) {
|
||||
// 获得接口对应的 HandlerMethod 集合
|
||||
RequestMappingHandlerMapping requestMappingHandlerMapping = (RequestMappingHandlerMapping)
|
||||
applicationContext.getBean("requestMappingHandlerMapping");
|
||||
Map<RequestMappingInfo, HandlerMethod> handlerMethodMap = requestMappingHandlerMapping.getHandlerMethods();
|
||||
// 获得有 @TenantIgnore 注解的接口
|
||||
for (Map.Entry<RequestMappingInfo, HandlerMethod> entry : handlerMethodMap.entrySet()) {
|
||||
HandlerMethod handlerMethod = entry.getValue();
|
||||
if (!handlerMethod.hasMethodAnnotation(TenantIgnore.class)) {
|
||||
continue;
|
||||
}
|
||||
// 添加到忽略的 URL 中
|
||||
if (entry.getKey().getPatternsCondition() != null) {
|
||||
tenantProperties.getIgnoreUrls().addAll(entry.getKey().getPatternsCondition().getPatterns());
|
||||
}
|
||||
if (entry.getKey().getPathPatternsCondition() != null) {
|
||||
tenantProperties.getIgnoreUrls().addAll(
|
||||
convertList(entry.getKey().getPathPatternsCondition().getPatterns(), PathPattern::getPatternString));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Bean
|
||||
public TenantVisitContextInterceptor tenantVisitContextInterceptor(TenantProperties tenantProperties,
|
||||
SecurityFrameworkService securityFrameworkService) {
|
||||
@ -146,12 +120,42 @@ public class YudaoTenantAutoConfiguration {
|
||||
GlobalExceptionHandler globalExceptionHandler,
|
||||
TenantFrameworkService tenantFrameworkService) {
|
||||
FilterRegistrationBean<TenantSecurityWebFilter> registrationBean = new FilterRegistrationBean<>();
|
||||
registrationBean.setFilter(new TenantSecurityWebFilter(tenantProperties, webProperties,
|
||||
registrationBean.setFilter(new TenantSecurityWebFilter(webProperties, tenantProperties, getTenantIgnoreUrls(),
|
||||
globalExceptionHandler, tenantFrameworkService));
|
||||
registrationBean.setOrder(WebFilterOrderEnum.TENANT_SECURITY_FILTER);
|
||||
return registrationBean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 如果 Controller 接口上,有 {@link TenantIgnore} 注解,则添加到忽略租户的 URL 集合中
|
||||
*
|
||||
* @return 忽略租户的 URL 集合
|
||||
*/
|
||||
private Set<String> getTenantIgnoreUrls() {
|
||||
Set<String> ignoreUrls = new HashSet<>();
|
||||
// 获得接口对应的 HandlerMethod 集合
|
||||
RequestMappingHandlerMapping requestMappingHandlerMapping = (RequestMappingHandlerMapping)
|
||||
applicationContext.getBean("requestMappingHandlerMapping");
|
||||
Map<RequestMappingInfo, HandlerMethod> handlerMethodMap = requestMappingHandlerMapping.getHandlerMethods();
|
||||
// 获得有 @TenantIgnore 注解的接口
|
||||
for (Map.Entry<RequestMappingInfo, HandlerMethod> entry : handlerMethodMap.entrySet()) {
|
||||
HandlerMethod handlerMethod = entry.getValue();
|
||||
if (!handlerMethod.hasMethodAnnotation(TenantIgnore.class) // 方法级
|
||||
&& !handlerMethod.getBeanType().isAnnotationPresent(TenantIgnore.class)) { // 接口级
|
||||
continue;
|
||||
}
|
||||
// 添加到忽略的 URL 中
|
||||
if (entry.getKey().getPatternsCondition() != null) {
|
||||
ignoreUrls.addAll(entry.getKey().getPatternsCondition().getPatterns());
|
||||
}
|
||||
if (entry.getKey().getPathPatternsCondition() != null) {
|
||||
ignoreUrls.addAll(
|
||||
convertList(entry.getKey().getPathPatternsCondition().getPatterns(), PathPattern::getPatternString));
|
||||
}
|
||||
}
|
||||
return ignoreUrls;
|
||||
}
|
||||
|
||||
// ========== MQ ==========
|
||||
|
||||
@Bean
|
||||
|
||||
@ -75,7 +75,7 @@ public class TenantDatabaseInterceptor implements TenantLineHandler {
|
||||
if (TenantBaseDO.class.isAssignableFrom(tableInfo.getEntityType())) {
|
||||
return false;
|
||||
}
|
||||
// 如果添加了 @TenantIgnore 注解,显然也不忽略租户
|
||||
// 如果添加了 @TenantIgnore 注解,则忽略租户
|
||||
TenantIgnore tenantIgnore = tableInfo.getEntityType().getAnnotation(TenantIgnore.class);
|
||||
return tenantIgnore != null;
|
||||
}
|
||||
|
||||
@ -12,6 +12,7 @@ import org.springframework.beans.factory.config.BeanPostProcessor;
|
||||
public class TenantRabbitMQInitializer implements BeanPostProcessor {
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("PatternVariableCanBeUsed")
|
||||
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
|
||||
if (bean instanceof RabbitTemplate) {
|
||||
RabbitTemplate rabbitTemplate = (RabbitTemplate) bean;
|
||||
@ -20,4 +21,4 @@ public class TenantRabbitMQInitializer implements BeanPostProcessor {
|
||||
return bean;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,6 +17,7 @@ import org.springframework.beans.factory.config.BeanPostProcessor;
|
||||
public class TenantRocketMQInitializer implements BeanPostProcessor {
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("PatternVariableCanBeUsed")
|
||||
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
|
||||
if (bean instanceof DefaultRocketMQListenerContainer) {
|
||||
DefaultRocketMQListenerContainer container = (DefaultRocketMQListenerContainer) bean;
|
||||
@ -50,4 +51,4 @@ public class TenantRocketMQInitializer implements BeanPostProcessor {
|
||||
consumerImpl.registerConsumeMessageHook(new TenantRocketMQConsumeMessageHook());
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,6 +21,7 @@ import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 多租户 Security Web 过滤器
|
||||
@ -35,17 +36,26 @@ public class TenantSecurityWebFilter extends ApiRequestFilter {
|
||||
|
||||
private final TenantProperties tenantProperties;
|
||||
|
||||
/**
|
||||
* 允许忽略租户的 URL 列表
|
||||
*
|
||||
* 目的:解决 <a href="https://gitee.com/zhijiantianya/yudao-cloud/issues/ICUQL9">修改配置会导致 @TenantIgnore Controller 接口过滤失效</>
|
||||
*/
|
||||
private final Set<String> ignoreUrls;
|
||||
|
||||
private final AntPathMatcher pathMatcher;
|
||||
|
||||
private final GlobalExceptionHandler globalExceptionHandler;
|
||||
private final TenantFrameworkService tenantFrameworkService;
|
||||
|
||||
public TenantSecurityWebFilter(TenantProperties tenantProperties,
|
||||
WebProperties webProperties,
|
||||
public TenantSecurityWebFilter(WebProperties webProperties,
|
||||
TenantProperties tenantProperties,
|
||||
Set<String> ignoreUrls,
|
||||
GlobalExceptionHandler globalExceptionHandler,
|
||||
TenantFrameworkService tenantFrameworkService) {
|
||||
super(webProperties);
|
||||
this.tenantProperties = tenantProperties;
|
||||
this.ignoreUrls = ignoreUrls;
|
||||
this.pathMatcher = new AntPathMatcher();
|
||||
this.globalExceptionHandler = globalExceptionHandler;
|
||||
this.tenantFrameworkService = tenantFrameworkService;
|
||||
@ -101,13 +111,20 @@ public class TenantSecurityWebFilter extends ApiRequestFilter {
|
||||
}
|
||||
|
||||
private boolean isIgnoreUrl(HttpServletRequest request) {
|
||||
String apiUri = request.getRequestURI().substring(request.getContextPath().length());
|
||||
// 快速匹配,保证性能
|
||||
if (CollUtil.contains(tenantProperties.getIgnoreUrls(), request.getRequestURI())) {
|
||||
if (CollUtil.contains(tenantProperties.getIgnoreUrls(), apiUri)
|
||||
|| CollUtil.contains(ignoreUrls, apiUri)) {
|
||||
return true;
|
||||
}
|
||||
// 逐个 Ant 路径匹配
|
||||
for (String url : tenantProperties.getIgnoreUrls()) {
|
||||
if (pathMatcher.match(url, request.getRequestURI())) {
|
||||
if (pathMatcher.match(url, apiUri)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
for (String url : ignoreUrls) {
|
||||
if (pathMatcher.match(url, apiUri)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,6 +21,7 @@ public class YudaoAsyncAutoConfiguration {
|
||||
return new BeanPostProcessor() {
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("PatternVariableCanBeUsed")
|
||||
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
|
||||
// 处理 ThreadPoolTaskExecutor
|
||||
if (bean instanceof ThreadPoolTaskExecutor) {
|
||||
|
||||
@ -1,11 +1,7 @@
|
||||
package cn.iocoder.yudao.framework.tracer.config;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum;
|
||||
import cn.iocoder.yudao.framework.tracer.core.aop.BizTraceAspect;
|
||||
import cn.iocoder.yudao.framework.tracer.core.filter.TraceFilter;
|
||||
import io.opentracing.Tracer;
|
||||
import io.opentracing.util.GlobalTracer;
|
||||
import org.apache.skywalking.apm.toolkit.opentracing.SkywalkingTracer;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
@ -20,31 +16,28 @@ import org.springframework.context.annotation.Bean;
|
||||
*/
|
||||
@AutoConfiguration
|
||||
@ConditionalOnClass(name = {
|
||||
"org.apache.skywalking.apm.toolkit.opentracing.SkywalkingTracer",
|
||||
"io.opentracing.Tracer"
|
||||
"org.apache.skywalking.apm.toolkit.opentracing.SkywalkingTracer", // 来自 apm-toolkit-opentracing.jar
|
||||
// "io.opentracing.Tracer", // 来自 opentracing-api.jar
|
||||
"javax.servlet.Filter"
|
||||
})
|
||||
@EnableConfigurationProperties(TracerProperties.class)
|
||||
@ConditionalOnProperty(prefix = "yudao.tracer", value = "enable", matchIfMissing = true)
|
||||
public class YudaoTracerAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
public TracerProperties bizTracerProperties() {
|
||||
return new TracerProperties();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public BizTraceAspect bizTracingAop() {
|
||||
return new BizTraceAspect(tracer());
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Tracer tracer() {
|
||||
// 创建 SkywalkingTracer 对象
|
||||
SkywalkingTracer tracer = new SkywalkingTracer();
|
||||
// 设置为 GlobalTracer 的追踪器
|
||||
GlobalTracer.registerIfAbsent(tracer);
|
||||
return tracer;
|
||||
}
|
||||
// TODO @芋艿:skywalking 不兼容最新的 opentracing 版本。同时,opentracing 也停止了维护,尬住了!后续换 opentelemetry 即可!
|
||||
// @Bean
|
||||
// public BizTraceAspect bizTracingAop() {
|
||||
// return new BizTraceAspect(tracer());
|
||||
// }
|
||||
//
|
||||
// @Bean
|
||||
// public Tracer tracer() {
|
||||
// // 创建 SkywalkingTracer 对象
|
||||
// SkywalkingTracer tracer = new SkywalkingTracer();
|
||||
// // 设置为 GlobalTracer 的追踪器
|
||||
// GlobalTracer.registerIfAbsent(tracer);
|
||||
// return tracer;
|
||||
// }
|
||||
|
||||
/**
|
||||
* 创建 TraceFilter 过滤器,响应 header 设置 traceId
|
||||
|
||||
@ -69,9 +69,8 @@ public class YudaoRedisMQConsumerAutoConfiguration {
|
||||
@ConditionalOnBean(AbstractRedisStreamMessageListener.class) // 只有 AbstractStreamMessageListener 存在的时候,才需要注册 Redis pubsub 监听
|
||||
public RedisPendingMessageResendJob redisPendingMessageResendJob(List<AbstractRedisStreamMessageListener<?>> listeners,
|
||||
RedisMQTemplate redisTemplate,
|
||||
@Value("${spring.application.name}") String groupName,
|
||||
RedissonClient redissonClient) {
|
||||
return new RedisPendingMessageResendJob(listeners, redisTemplate, groupName, redissonClient);
|
||||
return new RedisPendingMessageResendJob(listeners, redisTemplate, redissonClient);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -141,14 +140,14 @@ public class YudaoRedisMQConsumerAutoConfiguration {
|
||||
*
|
||||
* @return 消费者名字
|
||||
*/
|
||||
private static String buildConsumerName() {
|
||||
public static String buildConsumerName() {
|
||||
return String.format("%s@%d", SystemUtil.getHostInfo().getAddress(), SystemUtil.getCurrentPID());
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验 Redis 版本号,是否满足最低的版本号要求!
|
||||
*/
|
||||
private static void checkRedisVersion(RedisTemplate<String, ?> redisTemplate) {
|
||||
public static void checkRedisVersion(RedisTemplate<String, ?> redisTemplate) {
|
||||
// 获得 Redis 版本
|
||||
Properties info = redisTemplate.execute((RedisCallback<Properties>) RedisServerCommands::info);
|
||||
String version = MapUtil.getStr(info, "redis_version");
|
||||
|
||||
@ -35,7 +35,6 @@ public class RedisPendingMessageResendJob {
|
||||
|
||||
private final List<AbstractRedisStreamMessageListener<?>> listeners;
|
||||
private final RedisMQTemplate redisTemplate;
|
||||
private final String groupName;
|
||||
private final RedissonClient redissonClient;
|
||||
|
||||
/**
|
||||
@ -64,13 +63,13 @@ public class RedisPendingMessageResendJob {
|
||||
private void execute() {
|
||||
StreamOperations<String, Object, Object> ops = redisTemplate.getRedisTemplate().opsForStream();
|
||||
listeners.forEach(listener -> {
|
||||
PendingMessagesSummary pendingMessagesSummary = Objects.requireNonNull(ops.pending(listener.getStreamKey(), groupName));
|
||||
PendingMessagesSummary pendingMessagesSummary = Objects.requireNonNull(ops.pending(listener.getStreamKey(), listener.getGroup()));
|
||||
// 每个消费者的 pending 队列消息数量
|
||||
Map<String, Long> pendingMessagesPerConsumer = pendingMessagesSummary.getPendingMessagesPerConsumer();
|
||||
pendingMessagesPerConsumer.forEach((consumerName, pendingMessageCount) -> {
|
||||
log.info("[processPendingMessage][消费者({}) 消息数量({})]", consumerName, pendingMessageCount);
|
||||
// 每个消费者的 pending消息的详情信息
|
||||
PendingMessages pendingMessages = ops.pending(listener.getStreamKey(), Consumer.from(groupName, consumerName), Range.unbounded(), pendingMessageCount);
|
||||
PendingMessages pendingMessages = ops.pending(listener.getStreamKey(), Consumer.from(listener.getGroup(), consumerName), Range.unbounded(), pendingMessageCount);
|
||||
if (pendingMessages.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
@ -91,7 +90,7 @@ public class RedisPendingMessageResendJob {
|
||||
.ofObject(records.get(0).getValue()) // 设置内容
|
||||
.withStreamKey(listener.getStreamKey()));
|
||||
// ack 消息消费完成
|
||||
redisTemplate.getRedisTemplate().opsForStream().acknowledge(groupName, records.get(0));
|
||||
redisTemplate.getRedisTemplate().opsForStream().acknowledge(listener.getGroup(), records.get(0));
|
||||
log.info("[processPendingMessage][消息({})重新投递成功]", records.get(0).getId());
|
||||
});
|
||||
});
|
||||
|
||||
@ -53,6 +53,12 @@ public abstract class AbstractRedisStreamMessageListener<T extends AbstractRedis
|
||||
this.streamKey = messageType.getDeclaredConstructor().newInstance().getStreamKey();
|
||||
}
|
||||
|
||||
protected AbstractRedisStreamMessageListener(String streamKey, String group) {
|
||||
this.messageType = null;
|
||||
this.streamKey = streamKey;
|
||||
this.group = group;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessage(ObjectRecord<String, String> message) {
|
||||
// 消费消息
|
||||
|
||||
@ -1,16 +1,20 @@
|
||||
package cn.iocoder.yudao.framework.mybatis.config;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.handler.DefaultDBFieldHandler;
|
||||
import com.baomidou.mybatisplus.annotation.DbType;
|
||||
import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration;
|
||||
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
|
||||
import com.baomidou.mybatisplus.core.incrementer.IKeyGenerator;
|
||||
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||
import com.baomidou.mybatisplus.extension.incrementer.*;
|
||||
import com.baomidou.mybatisplus.extension.parser.JsqlParserGlobal;
|
||||
import com.baomidou.mybatisplus.extension.parser.cache.JdkSerialCaffeineJsqlParseCache;
|
||||
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
@ -18,6 +22,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.core.env.ConfigurableEnvironment;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
@ -42,6 +47,8 @@ public class YudaoMybatisAutoConfiguration {
|
||||
public MybatisPlusInterceptor mybatisPlusInterceptor() {
|
||||
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
|
||||
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor()); // 分页插件
|
||||
// ↓↓↓ 按需开启,可能会影响到 updateBatch 的地方:例如说文件配置管理 ↓↓↓
|
||||
// mybatisPlusInterceptor.addInnerInterceptor(new BlockAttackInnerInterceptor()); // 拦截没有指定条件的 update 和 delete 语句
|
||||
return mybatisPlusInterceptor;
|
||||
}
|
||||
|
||||
@ -73,4 +80,15 @@ public class YudaoMybatisAutoConfiguration {
|
||||
throw new IllegalArgumentException(StrUtil.format("DbType{} 找不到合适的 IKeyGenerator 实现类", dbType));
|
||||
}
|
||||
|
||||
@Bean
|
||||
public JacksonTypeHandler jacksonTypeHandler(List<ObjectMapper> objectMappers) {
|
||||
// 特殊:设置 JacksonTypeHandler 的 ObjectMapper!
|
||||
ObjectMapper objectMapper = CollUtil.getFirst(objectMappers);
|
||||
if (objectMapper == null) {
|
||||
objectMapper = JsonUtils.getObjectMapper();
|
||||
}
|
||||
JacksonTypeHandler.setObjectMapper(objectMapper);
|
||||
return new JacksonTypeHandler(Object.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -18,6 +18,7 @@ import java.util.Objects;
|
||||
public class DefaultDBFieldHandler implements MetaObjectHandler {
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("PatternVariableCanBeUsed")
|
||||
public void insertFill(MetaObject metaObject) {
|
||||
if (Objects.nonNull(metaObject) && metaObject.getOriginalObject() instanceof BaseDO) {
|
||||
BaseDO baseDO = (BaseDO) metaObject.getOriginalObject();
|
||||
|
||||
@ -9,6 +9,7 @@ import cn.iocoder.yudao.framework.common.pojo.SortingField;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.enums.DbTypeEnum;
|
||||
import com.baomidou.mybatisplus.annotation.DbType;
|
||||
import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.OrderItem;
|
||||
import com.baomidou.mybatisplus.core.toolkit.StringPool;
|
||||
@ -47,16 +48,36 @@ public class MyBatisUtils {
|
||||
return page;
|
||||
}
|
||||
|
||||
@SuppressWarnings("PatternVariableCanBeUsed")
|
||||
public static <T> void addOrder(Wrapper<T> wrapper, Collection<SortingField> sortingFields) {
|
||||
if (CollUtil.isEmpty(sortingFields)) {
|
||||
return;
|
||||
}
|
||||
QueryWrapper<T> query = (QueryWrapper<T>) wrapper;
|
||||
for (SortingField sortingField : sortingFields) {
|
||||
query.orderBy(true,
|
||||
SortingField.ORDER_ASC.equals(sortingField.getOrder()),
|
||||
StrUtil.toUnderlineCase(sortingField.getField()));
|
||||
if (wrapper instanceof QueryWrapper) {
|
||||
QueryWrapper<T> query = (QueryWrapper<T>) wrapper;
|
||||
for (SortingField sortingField : sortingFields) {
|
||||
query.orderBy(true,
|
||||
SortingField.ORDER_ASC.equals(sortingField.getOrder()),
|
||||
StrUtil.toUnderlineCase(sortingField.getField()));
|
||||
}
|
||||
} else if (wrapper instanceof LambdaQueryWrapper) {
|
||||
// LambdaQueryWrapper 不直接支持字符串字段排序,使用 last 方法拼接 ORDER BY
|
||||
LambdaQueryWrapper<T> lambdaQuery = (LambdaQueryWrapper<T>) wrapper;
|
||||
StringBuilder orderBy = new StringBuilder();
|
||||
for (SortingField sortingField : sortingFields) {
|
||||
if (StrUtil.isNotEmpty(orderBy)) {
|
||||
orderBy.append(", ");
|
||||
}
|
||||
orderBy.append(StrUtil.toUnderlineCase(sortingField.getField()))
|
||||
.append(" ")
|
||||
.append(SortingField.ORDER_ASC.equals(sortingField.getOrder()) ? "ASC" : "DESC");
|
||||
}
|
||||
lambdaQuery.last("ORDER BY " + orderBy);
|
||||
// 另外个思路:https://blog.csdn.net/m0_59084856/article/details/138450913
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported wrapper type: " + wrapper.getClass().getName());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -39,7 +39,7 @@ public class RateLimiterAspect {
|
||||
|
||||
@Before("@annotation(rateLimiter)")
|
||||
public void beforePointCut(JoinPoint joinPoint, RateLimiter rateLimiter) {
|
||||
// 获得 IdempotentKeyResolver 对象
|
||||
// 获得 RateLimiterKeyResolver 对象
|
||||
RateLimiterKeyResolver keyResolver = keyResolvers.get(rateLimiter.keyResolver());
|
||||
Assert.notNull(keyResolver, "找不到对应的 RateLimiterKeyResolver");
|
||||
// 解析 Key
|
||||
|
||||
@ -165,7 +165,8 @@ public class YudaoWebSecurityConfigurerAdapter {
|
||||
// 获得有 @PermitAll 注解的接口
|
||||
for (Map.Entry<RequestMappingInfo, HandlerMethod> entry : handlerMethodMap.entrySet()) {
|
||||
HandlerMethod handlerMethod = entry.getValue();
|
||||
if (!handlerMethod.hasMethodAnnotation(PermitAll.class)) {
|
||||
if (!handlerMethod.hasMethodAnnotation(PermitAll.class) // 方法级
|
||||
&& !handlerMethod.getBeanType().isAnnotationPresent(PermitAll.class)) { // 接口级
|
||||
continue;
|
||||
}
|
||||
Set<String> urls = new HashSet<>();
|
||||
|
||||
@ -53,7 +53,13 @@
|
||||
<scope>provided</scope> <!-- 设置为 provided,主要是 GlobalExceptionHandler 使用 -->
|
||||
</dependency>
|
||||
|
||||
<!-- xss -->
|
||||
<!-- 工具类相关 -->
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
<scope>provided</scope> <!-- 设置为 provided,只有工具类需要使用到 -->
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.jsoup</groupId>
|
||||
<artifactId>jsoup</artifactId>
|
||||
|
||||
@ -0,0 +1,71 @@
|
||||
package cn.iocoder.yudao.framework.encrypt.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
/**
|
||||
* HTTP API 加解密配置
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@ConfigurationProperties(prefix = "yudao.api-encrypt")
|
||||
@Validated
|
||||
@Data
|
||||
public class ApiEncryptProperties {
|
||||
|
||||
/**
|
||||
* 是否开启
|
||||
*/
|
||||
@NotNull(message = "是否开启不能为空")
|
||||
private Boolean enable;
|
||||
|
||||
/**
|
||||
* 请求头(响应头)名称
|
||||
*
|
||||
* 1. 如果该请求头非空,则表示请求参数已被「前端」加密,「后端」需要解密
|
||||
* 2. 如果该响应头非空,则表示响应结果已被「后端」加密,「前端」需要解密
|
||||
*/
|
||||
@NotEmpty(message = "请求头(响应头)名称不能为空")
|
||||
private String header = "X-Api-Encrypt";
|
||||
|
||||
/**
|
||||
* 对称加密算法,用于请求/响应的加解密
|
||||
*
|
||||
* 目前支持
|
||||
* 【对称加密】:
|
||||
* 1. {@link cn.hutool.crypto.symmetric.SymmetricAlgorithm#AES}
|
||||
* 2. {@link cn.hutool.crypto.symmetric.SM4#ALGORITHM_NAME} (需要自己二次开发,成本低)
|
||||
* 【非对称加密】
|
||||
* 1. {@link cn.hutool.crypto.asymmetric.AsymmetricAlgorithm#RSA}
|
||||
* 2. {@link cn.hutool.crypto.asymmetric.SM2} (需要自己二次开发,成本低)
|
||||
*
|
||||
* @see <a href="https://help.aliyun.com/zh/ssl-certificate/what-are-a-public-key-and-a-private-key">什么是公钥和私钥?</a>
|
||||
*/
|
||||
@NotEmpty(message = "对称加密算法不能为空")
|
||||
private String algorithm;
|
||||
|
||||
/**
|
||||
* 请求的解密密钥
|
||||
*
|
||||
* 注意:
|
||||
* 1. 如果是【对称加密】时,它「后端」对应的是“密钥”。对应的,「前端」也对应的也是“密钥”。
|
||||
* 2. 如果是【非对称加密】时,它「后端」对应的是“私钥”。对应的,「前端」对应的是“公钥”。(重要!!!)
|
||||
*/
|
||||
@NotEmpty(message = "请求的解密密钥不能为空")
|
||||
private String requestKey;
|
||||
|
||||
/**
|
||||
* 响应的加密密钥
|
||||
*
|
||||
* 注意:
|
||||
* 1. 如果是【对称加密】时,它「后端」对应的是“密钥”。对应的,「前端」也对应的也是“密钥”。
|
||||
* 2. 如果是【非对称加密】时,它「后端」对应的是“公钥”。对应的,「前端」对应的是“私钥”。(重要!!!)
|
||||
*/
|
||||
@NotEmpty(message = "响应的加密密钥不能为空")
|
||||
private String responseKey;
|
||||
|
||||
}
|
||||
@ -0,0 +1,34 @@
|
||||
package cn.iocoder.yudao.framework.encrypt.config;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum;
|
||||
import cn.iocoder.yudao.framework.encrypt.core.filter.ApiEncryptFilter;
|
||||
import cn.iocoder.yudao.framework.web.config.WebProperties;
|
||||
import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
|
||||
|
||||
import static cn.iocoder.yudao.framework.web.config.YudaoWebAutoConfiguration.createFilterBean;
|
||||
|
||||
@AutoConfiguration
|
||||
@Slf4j
|
||||
@EnableConfigurationProperties(ApiEncryptProperties.class)
|
||||
@ConditionalOnProperty(prefix = "yudao.api-encrypt", name = "enable", havingValue = "true")
|
||||
public class YudaoApiEncryptAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
public FilterRegistrationBean<ApiEncryptFilter> apiEncryptFilter(WebProperties webProperties,
|
||||
ApiEncryptProperties apiEncryptProperties,
|
||||
RequestMappingHandlerMapping requestMappingHandlerMapping,
|
||||
GlobalExceptionHandler globalExceptionHandler) {
|
||||
ApiEncryptFilter filter = new ApiEncryptFilter(webProperties, apiEncryptProperties,
|
||||
requestMappingHandlerMapping, globalExceptionHandler);
|
||||
return createFilterBean(filter, WebFilterOrderEnum.API_ENCRYPT_FILTER);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
package cn.iocoder.yudao.framework.encrypt.core.annotation;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* HTTP API 加解密注解
|
||||
*/
|
||||
@Documented
|
||||
@Target({ElementType.TYPE, ElementType.METHOD})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface ApiEncrypt {
|
||||
|
||||
/**
|
||||
* 是否对请求参数进行解密,默认 true
|
||||
*/
|
||||
boolean request() default true;
|
||||
|
||||
/**
|
||||
* 是否对响应结果进行加密,默认 true
|
||||
*/
|
||||
boolean response() default true;
|
||||
|
||||
}
|
||||
@ -0,0 +1,86 @@
|
||||
package cn.iocoder.yudao.framework.encrypt.core.filter;
|
||||
|
||||
import cn.hutool.core.io.IoUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.crypto.asymmetric.AsymmetricDecryptor;
|
||||
import cn.hutool.crypto.asymmetric.KeyType;
|
||||
import cn.hutool.crypto.symmetric.SymmetricDecryptor;
|
||||
|
||||
import javax.servlet.ReadListener;
|
||||
import javax.servlet.ServletInputStream;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletRequestWrapper;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
|
||||
/**
|
||||
* 解密请求 {@link HttpServletRequestWrapper} 实现类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class ApiDecryptRequestWrapper extends HttpServletRequestWrapper {
|
||||
|
||||
private final byte[] body;
|
||||
|
||||
public ApiDecryptRequestWrapper(HttpServletRequest request,
|
||||
SymmetricDecryptor symmetricDecryptor,
|
||||
AsymmetricDecryptor asymmetricDecryptor) throws IOException {
|
||||
super(request);
|
||||
// 读取 body,允许 HEX、BASE64 传输
|
||||
String requestBody = StrUtil.utf8Str(
|
||||
IoUtil.readBytes(request.getInputStream(), false));
|
||||
|
||||
// 解密 body
|
||||
body = symmetricDecryptor != null ? symmetricDecryptor.decrypt(requestBody)
|
||||
: asymmetricDecryptor.decrypt(requestBody, KeyType.PrivateKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BufferedReader getReader() {
|
||||
return new BufferedReader(new InputStreamReader(this.getInputStream()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getContentLength() {
|
||||
return body.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getContentLengthLong() {
|
||||
return body.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServletInputStream getInputStream() {
|
||||
ByteArrayInputStream stream = new ByteArrayInputStream(body);
|
||||
return new ServletInputStream() {
|
||||
|
||||
@Override
|
||||
public int read() {
|
||||
return stream.read();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int available() {
|
||||
return body.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFinished() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReady() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setReadListener(ReadListener readListener) {
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,161 @@
|
||||
package cn.iocoder.yudao.framework.encrypt.core.filter;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.crypto.SecureUtil;
|
||||
import cn.hutool.crypto.asymmetric.AsymmetricDecryptor;
|
||||
import cn.hutool.crypto.asymmetric.AsymmetricEncryptor;
|
||||
import cn.hutool.crypto.symmetric.SymmetricDecryptor;
|
||||
import cn.hutool.crypto.symmetric.SymmetricEncryptor;
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
|
||||
import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
|
||||
import cn.iocoder.yudao.framework.encrypt.config.ApiEncryptProperties;
|
||||
import cn.iocoder.yudao.framework.encrypt.core.annotation.ApiEncrypt;
|
||||
import cn.iocoder.yudao.framework.web.config.WebProperties;
|
||||
import cn.iocoder.yudao.framework.web.core.filter.ApiRequestFilter;
|
||||
import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.servlet.HandlerExecutionChain;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
|
||||
import org.springframework.web.util.ServletRequestPathUtils;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
|
||||
|
||||
/**
|
||||
* API 加密过滤器,处理 {@link ApiEncrypt} 注解。
|
||||
*
|
||||
* 1. 解密请求参数
|
||||
* 2. 加密响应结果
|
||||
*
|
||||
* 疑问:为什么不使用 SpringMVC 的 RequestBodyAdvice 或 ResponseBodyAdvice 机制呢?
|
||||
* 回答:考虑到项目中会记录访问日志、异常日志,以及 HTTP API 签名等场景,最好是全局级、且提前做解析!!!
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
public class ApiEncryptFilter extends ApiRequestFilter {
|
||||
|
||||
private final ApiEncryptProperties apiEncryptProperties;
|
||||
|
||||
private final RequestMappingHandlerMapping requestMappingHandlerMapping;
|
||||
|
||||
private final GlobalExceptionHandler globalExceptionHandler;
|
||||
|
||||
private final SymmetricDecryptor requestSymmetricDecryptor;
|
||||
private final AsymmetricDecryptor requestAsymmetricDecryptor;
|
||||
|
||||
private final SymmetricEncryptor responseSymmetricEncryptor;
|
||||
private final AsymmetricEncryptor responseAsymmetricEncryptor;
|
||||
|
||||
public ApiEncryptFilter(WebProperties webProperties,
|
||||
ApiEncryptProperties apiEncryptProperties,
|
||||
RequestMappingHandlerMapping requestMappingHandlerMapping,
|
||||
GlobalExceptionHandler globalExceptionHandler) {
|
||||
super(webProperties);
|
||||
this.apiEncryptProperties = apiEncryptProperties;
|
||||
this.requestMappingHandlerMapping = requestMappingHandlerMapping;
|
||||
this.globalExceptionHandler = globalExceptionHandler;
|
||||
if (StrUtil.equalsIgnoreCase(apiEncryptProperties.getAlgorithm(), "AES")) {
|
||||
this.requestSymmetricDecryptor = SecureUtil.aes(StrUtil.utf8Bytes(apiEncryptProperties.getRequestKey()));
|
||||
this.requestAsymmetricDecryptor = null;
|
||||
this.responseSymmetricEncryptor = SecureUtil.aes(StrUtil.utf8Bytes(apiEncryptProperties.getResponseKey()));
|
||||
this.responseAsymmetricEncryptor = null;
|
||||
} else if (StrUtil.equalsIgnoreCase(apiEncryptProperties.getAlgorithm(), "RSA")) {
|
||||
this.requestSymmetricDecryptor = null;
|
||||
this.requestAsymmetricDecryptor = SecureUtil.rsa(apiEncryptProperties.getRequestKey(), null);
|
||||
this.responseSymmetricEncryptor = null;
|
||||
this.responseAsymmetricEncryptor = SecureUtil.rsa(null, apiEncryptProperties.getResponseKey());
|
||||
} else {
|
||||
// 补充说明:如果要支持 SM2、SM4 等算法,可在此处增加对应实例的创建,并添加相应的 Maven 依赖即可。
|
||||
throw new IllegalArgumentException("不支持的加密算法:" + apiEncryptProperties.getAlgorithm());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("NullableProblems")
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
|
||||
throws ServletException, IOException {
|
||||
// 获取 @ApiEncrypt 注解
|
||||
ApiEncrypt apiEncrypt = getApiEncrypt(request);
|
||||
boolean requestEnable = apiEncrypt != null && apiEncrypt.request();
|
||||
boolean responseEnable = apiEncrypt != null && apiEncrypt.response();
|
||||
String encryptHeader = request.getHeader(apiEncryptProperties.getHeader());
|
||||
if (!requestEnable && !responseEnable && StrUtil.isBlank(encryptHeader)) {
|
||||
chain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. 解密请求
|
||||
if (ObjectUtils.equalsAny(HttpMethod.valueOf(request.getMethod()),
|
||||
HttpMethod.POST, HttpMethod.PUT, HttpMethod.DELETE)) {
|
||||
try {
|
||||
if (StrUtil.isNotBlank(encryptHeader)) {
|
||||
request = new ApiDecryptRequestWrapper(request,
|
||||
requestSymmetricDecryptor, requestAsymmetricDecryptor);
|
||||
} else if (requestEnable) {
|
||||
throw invalidParamException("请求未包含加密标头,请检查是否正确配置了加密标头");
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
CommonResult<?> result = globalExceptionHandler.allExceptionHandler(request, ex);
|
||||
ServletUtils.writeJSON(response, result);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 执行过滤器链
|
||||
if (responseEnable) {
|
||||
// 特殊:仅包装,最后执行。目的:Response 内容可以被重复读取!!!
|
||||
response = new ApiEncryptResponseWrapper(response);
|
||||
}
|
||||
chain.doFilter(request, response);
|
||||
|
||||
// 3. 加密响应(真正执行)
|
||||
if (responseEnable) {
|
||||
((ApiEncryptResponseWrapper) response).encrypt(apiEncryptProperties,
|
||||
responseSymmetricEncryptor, responseAsymmetricEncryptor);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 @ApiEncrypt 注解
|
||||
*
|
||||
* @param request 请求
|
||||
*/
|
||||
@SuppressWarnings("PatternVariableCanBeUsed")
|
||||
private ApiEncrypt getApiEncrypt(HttpServletRequest request) {
|
||||
try {
|
||||
// 特殊:兼容 SpringBoot 2.X 版本会报错的问题 https://t.zsxq.com/kqyiB
|
||||
if (!ServletRequestPathUtils.hasParsedRequestPath(request)) {
|
||||
ServletRequestPathUtils.parseAndCache(request);
|
||||
}
|
||||
|
||||
// 解析 @ApiEncrypt 注解
|
||||
HandlerExecutionChain mappingHandler = requestMappingHandlerMapping.getHandler(request);
|
||||
if (mappingHandler == null) {
|
||||
return null;
|
||||
}
|
||||
Object handler = mappingHandler.getHandler();
|
||||
if (handler instanceof HandlerMethod) {
|
||||
HandlerMethod handlerMethod = (HandlerMethod) handler;
|
||||
ApiEncrypt annotation = handlerMethod.getMethodAnnotation(ApiEncrypt.class);
|
||||
if (annotation == null) {
|
||||
annotation = handlerMethod.getBeanType().getAnnotation(ApiEncrypt.class);
|
||||
}
|
||||
return annotation;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("[getApiEncrypt][url({}/{}) 获取 @ApiEncrypt 注解失败]",
|
||||
request.getRequestURI(), request.getMethod(), e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,109 @@
|
||||
package cn.iocoder.yudao.framework.encrypt.core.filter;
|
||||
|
||||
import cn.hutool.crypto.asymmetric.AsymmetricEncryptor;
|
||||
import cn.hutool.crypto.asymmetric.KeyType;
|
||||
import cn.hutool.crypto.symmetric.SymmetricEncryptor;
|
||||
import cn.iocoder.yudao.framework.encrypt.config.ApiEncryptProperties;
|
||||
|
||||
import javax.servlet.ServletOutputStream;
|
||||
import javax.servlet.WriteListener;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.http.HttpServletResponseWrapper;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.PrintWriter;
|
||||
|
||||
/**
|
||||
* 加密响应 {@link HttpServletResponseWrapper} 实现类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class ApiEncryptResponseWrapper extends HttpServletResponseWrapper {
|
||||
|
||||
private final ByteArrayOutputStream byteArrayOutputStream;
|
||||
private final ServletOutputStream servletOutputStream;
|
||||
private final PrintWriter printWriter;
|
||||
|
||||
public ApiEncryptResponseWrapper(HttpServletResponse response) {
|
||||
super(response);
|
||||
this.byteArrayOutputStream = new ByteArrayOutputStream();
|
||||
this.servletOutputStream = this.getOutputStream();
|
||||
this.printWriter = new PrintWriter(new OutputStreamWriter(byteArrayOutputStream));
|
||||
}
|
||||
|
||||
public void encrypt(ApiEncryptProperties properties,
|
||||
SymmetricEncryptor symmetricEncryptor,
|
||||
AsymmetricEncryptor asymmetricEncryptor) throws IOException {
|
||||
// 1.1 清空 body
|
||||
HttpServletResponse response = (HttpServletResponse) this.getResponse();
|
||||
response.resetBuffer();
|
||||
// 1.2 获取 body
|
||||
this.flushBuffer();
|
||||
byte[] body = byteArrayOutputStream.toByteArray();
|
||||
|
||||
// 2. 加密 body
|
||||
String encryptedBody = symmetricEncryptor != null ? symmetricEncryptor.encryptBase64(body)
|
||||
: asymmetricEncryptor.encryptBase64(body, KeyType.PublicKey);
|
||||
response.getWriter().write(encryptedBody);
|
||||
|
||||
// 3. 添加加密 header 标识
|
||||
this.addHeader(properties.getHeader(), "true");
|
||||
// 特殊:特殊:https://juejin.cn/post/6867327674675625992
|
||||
this.addHeader("Access-Control-Expose-Headers", properties.getHeader());
|
||||
}
|
||||
|
||||
@Override
|
||||
public PrintWriter getWriter() {
|
||||
return printWriter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flushBuffer() throws IOException {
|
||||
if (servletOutputStream != null) {
|
||||
servletOutputStream.flush();
|
||||
}
|
||||
if (printWriter != null) {
|
||||
printWriter.flush();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
byteArrayOutputStream.reset();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServletOutputStream getOutputStream() {
|
||||
return new ServletOutputStream() {
|
||||
|
||||
@Override
|
||||
public boolean isReady() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setWriteListener(WriteListener writeListener) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(int b) {
|
||||
byteArrayOutputStream.write(b);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("NullableProblems")
|
||||
public void write(byte[] b) throws IOException {
|
||||
byteArrayOutputStream.write(b);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("NullableProblems")
|
||||
public void write(byte[] b, int off, int len) {
|
||||
byteArrayOutputStream.write(b, off, len);
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
/**
|
||||
* HTTP API 加密组件:支持 Request 和 Response 的加密、解密
|
||||
*/
|
||||
package cn.iocoder.yudao.framework.encrypt;
|
||||
@ -1,10 +1,10 @@
|
||||
package cn.iocoder.yudao.framework.jackson.config;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
||||
import cn.iocoder.yudao.framework.common.util.json.databind.NumberSerializer;
|
||||
import cn.iocoder.yudao.framework.common.util.json.databind.TimestampLocalDateTimeDeserializer;
|
||||
import cn.iocoder.yudao.framework.common.util.json.databind.TimestampLocalDateTimeSerializer;
|
||||
import com.fasterxml.jackson.databind.Module;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule;
|
||||
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
|
||||
@ -13,39 +13,65 @@ import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
|
||||
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.util.List;
|
||||
|
||||
@AutoConfiguration
|
||||
@AutoConfiguration(after = JacksonAutoConfiguration.class)
|
||||
@Slf4j
|
||||
public class YudaoJacksonAutoConfiguration {
|
||||
|
||||
/**
|
||||
* 从 Builder 源头定制(关键:使用 *ByType,避免 handledType 要求)
|
||||
*/
|
||||
@Bean
|
||||
public Jackson2ObjectMapperBuilderCustomizer ldtEpochMillisCustomizer() {
|
||||
return builder -> builder
|
||||
// Long -> Number
|
||||
.serializerByType(Long.class, NumberSerializer.INSTANCE)
|
||||
.serializerByType(Long.TYPE, NumberSerializer.INSTANCE)
|
||||
// LocalDate / LocalTime
|
||||
.serializerByType(LocalDate.class, LocalDateSerializer.INSTANCE)
|
||||
.deserializerByType(LocalDate.class, LocalDateDeserializer.INSTANCE)
|
||||
.serializerByType(LocalTime.class, LocalTimeSerializer.INSTANCE)
|
||||
.deserializerByType(LocalTime.class, LocalTimeDeserializer.INSTANCE)
|
||||
// LocalDateTime < - > EpochMillis
|
||||
.serializerByType(LocalDateTime.class, TimestampLocalDateTimeSerializer.INSTANCE)
|
||||
.deserializerByType(LocalDateTime.class, TimestampLocalDateTimeDeserializer.INSTANCE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 以 Bean 形式暴露 Module(Boot 会自动注册到所有 ObjectMapper)
|
||||
*/
|
||||
@Bean
|
||||
public Module timestampSupportModuleBean() {
|
||||
SimpleModule m = new SimpleModule("TimestampSupportModule");
|
||||
// Long -> Number,避免前端精度丢失
|
||||
m.addSerializer(Long.class, NumberSerializer.INSTANCE);
|
||||
m.addSerializer(Long.TYPE, NumberSerializer.INSTANCE);
|
||||
// LocalDate / LocalTime
|
||||
m.addSerializer(LocalDate.class, LocalDateSerializer.INSTANCE);
|
||||
m.addDeserializer(LocalDate.class, LocalDateDeserializer.INSTANCE);
|
||||
m.addSerializer(LocalTime.class, LocalTimeSerializer.INSTANCE);
|
||||
m.addDeserializer(LocalTime.class, LocalTimeDeserializer.INSTANCE);
|
||||
// LocalDateTime < - > EpochMillis
|
||||
m.addSerializer(LocalDateTime.class, TimestampLocalDateTimeSerializer.INSTANCE);
|
||||
m.addDeserializer(LocalDateTime.class, TimestampLocalDateTimeDeserializer.INSTANCE);
|
||||
return m;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化全局 JsonUtils,直接使用主 ObjectMapper
|
||||
*/
|
||||
@Bean
|
||||
@SuppressWarnings("InstantiationOfUtilityClass")
|
||||
public JsonUtils jsonUtils(List<ObjectMapper> objectMappers) {
|
||||
// 1.1 创建 SimpleModule 对象
|
||||
SimpleModule simpleModule = new SimpleModule();
|
||||
simpleModule
|
||||
// 新增 Long 类型序列化规则,数值超过 2^53-1,在 JS 会出现精度丢失问题,因此 Long 自动序列化为字符串类型
|
||||
.addSerializer(Long.class, NumberSerializer.INSTANCE)
|
||||
.addSerializer(Long.TYPE, NumberSerializer.INSTANCE)
|
||||
.addSerializer(LocalDate.class, LocalDateSerializer.INSTANCE)
|
||||
.addDeserializer(LocalDate.class, LocalDateDeserializer.INSTANCE)
|
||||
.addSerializer(LocalTime.class, LocalTimeSerializer.INSTANCE)
|
||||
.addDeserializer(LocalTime.class, LocalTimeDeserializer.INSTANCE)
|
||||
// 新增 LocalDateTime 序列化、反序列化规则,使用 Long 时间戳
|
||||
.addSerializer(LocalDateTime.class, TimestampLocalDateTimeSerializer.INSTANCE)
|
||||
.addDeserializer(LocalDateTime.class, TimestampLocalDateTimeDeserializer.INSTANCE);
|
||||
// 1.2 注册到 objectMapper
|
||||
objectMappers.forEach(objectMapper -> objectMapper.registerModule(simpleModule));
|
||||
|
||||
// 2. 设置 objectMapper 到 JsonUtils
|
||||
JsonUtils.init(CollUtil.getFirst(objectMappers));
|
||||
log.info("[init][初始化 JsonUtils 成功]");
|
||||
public JsonUtils jsonUtils(ObjectMapper objectMapper) {
|
||||
JsonUtils.init(objectMapper);
|
||||
log.debug("[init][初始化 JsonUtils 成功]");
|
||||
return new JsonUtils();
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,146 @@
|
||||
package cn.iocoder.yudao.framework.swagger.config;
|
||||
|
||||
import com.github.xiaoymin.knife4j.annotations.ApiSupport;
|
||||
import com.github.xiaoymin.knife4j.core.conf.ExtensionsConstants;
|
||||
import com.github.xiaoymin.knife4j.core.conf.GlobalConstants;
|
||||
import com.github.xiaoymin.knife4j.spring.configuration.Knife4jProperties;
|
||||
import com.github.xiaoymin.knife4j.spring.configuration.Knife4jSetting;
|
||||
import com.github.xiaoymin.knife4j.spring.extension.OpenApiExtensionResolver;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import io.swagger.v3.oas.models.OpenAPI;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.springdoc.core.SpringDocConfigProperties;
|
||||
import org.springdoc.core.customizers.GlobalOpenApiCustomizer;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.core.type.filter.AnnotationTypeFilter;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 增强扩展属性支持
|
||||
*
|
||||
* 参考 <a href="https://github.com/xiaoymin/knife4j/issues/913">Spring Boot 3.4 以上版本 /v3/api-docs 解决接口报错,依赖修复</a>
|
||||
*
|
||||
* @since 4.1.0
|
||||
* @author <a href="xiaoymin@foxmail.com">xiaoymin@foxmail.com</a>
|
||||
* 2022/12/11 22:40
|
||||
*/
|
||||
@Primary
|
||||
@Configuration
|
||||
@Slf4j
|
||||
public class Knife4jOpenApiCustomizer extends com.github.xiaoymin.knife4j.spring.extension.Knife4jOpenApiCustomizer
|
||||
implements GlobalOpenApiCustomizer {
|
||||
|
||||
final Knife4jProperties knife4jProperties;
|
||||
final SpringDocConfigProperties properties;
|
||||
|
||||
public Knife4jOpenApiCustomizer(Knife4jProperties knife4jProperties, SpringDocConfigProperties properties) {
|
||||
super(knife4jProperties,properties);
|
||||
this.knife4jProperties = knife4jProperties;
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void customise(OpenAPI openApi) {
|
||||
if (knife4jProperties.isEnable()) {
|
||||
Knife4jSetting setting = knife4jProperties.getSetting();
|
||||
OpenApiExtensionResolver openApiExtensionResolver = new OpenApiExtensionResolver(setting, knife4jProperties.getDocuments());
|
||||
// 解析初始化
|
||||
openApiExtensionResolver.start();
|
||||
Map<String, Object> objectMap = new HashMap<>();
|
||||
objectMap.put(GlobalConstants.EXTENSION_OPEN_SETTING_NAME, setting);
|
||||
objectMap.put(GlobalConstants.EXTENSION_OPEN_MARKDOWN_NAME, openApiExtensionResolver.getMarkdownFiles());
|
||||
openApi.addExtension(GlobalConstants.EXTENSION_OPEN_API_NAME, objectMap);
|
||||
addOrderExtension(openApi);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 往 OpenAPI 内 tags 字段添加 x-order 属性
|
||||
*
|
||||
* @param openApi openApi
|
||||
*/
|
||||
private void addOrderExtension(OpenAPI openApi) {
|
||||
if (CollectionUtils.isEmpty(properties.getGroupConfigs())) {
|
||||
return;
|
||||
}
|
||||
// 获取包扫描路径
|
||||
Set<String> packagesToScan =
|
||||
properties.getGroupConfigs().stream()
|
||||
.map(SpringDocConfigProperties.GroupConfig::getPackagesToScan)
|
||||
.filter(toScan -> !CollectionUtils.isEmpty(toScan))
|
||||
.flatMap(List::stream)
|
||||
.collect(Collectors.toSet());
|
||||
if (CollectionUtils.isEmpty(packagesToScan)) {
|
||||
return;
|
||||
}
|
||||
// 扫描包下被 ApiSupport 注解的 RestController Class
|
||||
Set<Class<?>> classes = packagesToScan.stream()
|
||||
.map(packageToScan -> scanPackageByAnnotation(packageToScan, RestController.class))
|
||||
.flatMap(Set::stream)
|
||||
.filter(clazz -> clazz.isAnnotationPresent(ApiSupport.class))
|
||||
.collect(Collectors.toSet());
|
||||
if (!CollectionUtils.isEmpty(classes)) {
|
||||
// ApiSupport oder 值存入 tagSortMap<Tag.name,ApiSupport.order>
|
||||
Map<String, Integer> tagOrderMap = new HashMap<>();
|
||||
classes.forEach(clazz -> {
|
||||
Tag tag = getTag(clazz);
|
||||
if (Objects.nonNull(tag)) {
|
||||
ApiSupport apiSupport = clazz.getAnnotation(ApiSupport.class);
|
||||
tagOrderMap.putIfAbsent(tag.name(), apiSupport.order());
|
||||
}
|
||||
});
|
||||
// 往 openApi tags 字段添加 x-order 增强属性
|
||||
if (openApi.getTags() != null) {
|
||||
openApi.getTags().forEach(tag -> {
|
||||
if (tagOrderMap.containsKey(tag.getName())) {
|
||||
tag.addExtension(ExtensionsConstants.EXTENSION_ORDER, tagOrderMap.get(tag.getName()));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Tag getTag(Class<?> clazz) {
|
||||
// 从类上获取
|
||||
Tag tag = clazz.getAnnotation(Tag.class);
|
||||
if (Objects.isNull(tag)) {
|
||||
// 从接口上获取
|
||||
Class<?>[] interfaces = clazz.getInterfaces();
|
||||
if (ArrayUtils.isNotEmpty(interfaces)) {
|
||||
for (Class<?> interfaceClazz : interfaces) {
|
||||
Tag anno = interfaceClazz.getAnnotation(Tag.class);
|
||||
if (Objects.nonNull(anno)) {
|
||||
tag = anno;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return tag;
|
||||
}
|
||||
|
||||
private Set<Class<?>> scanPackageByAnnotation(String packageName, final Class<? extends Annotation> annotationClass) {
|
||||
ClassPathScanningCandidateComponentProvider scanner =
|
||||
new ClassPathScanningCandidateComponentProvider(false);
|
||||
scanner.addIncludeFilter(new AnnotationTypeFilter(annotationClass));
|
||||
Set<Class<?>> classes = new HashSet<>();
|
||||
for (BeanDefinition beanDefinition : scanner.findCandidateComponents(packageName)) {
|
||||
try {
|
||||
Class<?> clazz = Class.forName(beanDefinition.getBeanClassName());
|
||||
classes.add(clazz);
|
||||
} catch (ClassNotFoundException ignore) {
|
||||
}
|
||||
}
|
||||
return classes;
|
||||
}
|
||||
|
||||
}
|
||||
@ -19,6 +19,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
|
||||
@ -42,6 +43,7 @@ import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.HEADER_
|
||||
@ConditionalOnClass({OpenAPI.class})
|
||||
@EnableConfigurationProperties(SwaggerProperties.class)
|
||||
@ConditionalOnProperty(prefix = "springdoc.api-docs", name = "enabled", havingValue = "true", matchIfMissing = true) // 设置为 false 时,禁用
|
||||
@Import(Knife4jOpenApiCustomizer.class)
|
||||
public class YudaoSwaggerAutoConfiguration {
|
||||
|
||||
// ========== 全局 OpenAPI 配置 ==========
|
||||
|
||||
@ -8,7 +8,6 @@ import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletRequestWrapper;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
|
||||
/**
|
||||
@ -29,12 +28,22 @@ public class CacheRequestBodyWrapper extends HttpServletRequestWrapper {
|
||||
}
|
||||
|
||||
@Override
|
||||
public BufferedReader getReader() throws IOException {
|
||||
public BufferedReader getReader() {
|
||||
return new BufferedReader(new InputStreamReader(this.getInputStream()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServletInputStream getInputStream() throws IOException {
|
||||
public int getContentLength() {
|
||||
return body.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getContentLengthLong() {
|
||||
return body.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServletInputStream getInputStream() {
|
||||
final ByteArrayInputStream inputStream = new ByteArrayInputStream(body);
|
||||
// 返回 ServletInputStream
|
||||
return new ServletInputStream() {
|
||||
|
||||
@ -16,6 +16,7 @@ import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils;
|
||||
import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
|
||||
import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
|
||||
import com.fasterxml.jackson.databind.exc.InvalidFormatException;
|
||||
import com.google.common.util.concurrent.UncheckedExecutionException;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||
@ -31,6 +32,7 @@ import org.springframework.web.bind.MissingServletRequestParameterException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
|
||||
import org.springframework.web.multipart.MaxUploadSizeExceededException;
|
||||
import org.springframework.web.servlet.NoHandlerFoundException;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
@ -91,6 +93,9 @@ public class GlobalExceptionHandler {
|
||||
if (ex instanceof ValidationException) {
|
||||
return validationException((ValidationException) ex);
|
||||
}
|
||||
if (ex instanceof MaxUploadSizeExceededException) {
|
||||
return maxUploadSizeExceededExceptionHandler((MaxUploadSizeExceededException) ex);
|
||||
}
|
||||
if (ex instanceof NoHandlerFoundException) {
|
||||
return noHandlerFoundExceptionHandler((NoHandlerFoundException) ex);
|
||||
}
|
||||
@ -173,6 +178,7 @@ public class GlobalExceptionHandler {
|
||||
* 例如说,接口上设置了 @RequestBody 实体中 xx 属性类型为 Integer,结果传递 xx 参数类型为 String
|
||||
*/
|
||||
@ExceptionHandler(HttpMessageNotReadableException.class)
|
||||
@SuppressWarnings("PatternVariableCanBeUsed")
|
||||
public CommonResult<?> methodArgumentTypeInvalidFormatExceptionHandler(HttpMessageNotReadableException ex) {
|
||||
log.warn("[methodArgumentTypeInvalidFormatExceptionHandler]", ex);
|
||||
if (ex.getCause() instanceof InvalidFormatException) {
|
||||
@ -205,6 +211,14 @@ public class GlobalExceptionHandler {
|
||||
return CommonResult.error(BAD_REQUEST);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理上传文件过大异常
|
||||
*/
|
||||
@ExceptionHandler(MaxUploadSizeExceededException.class)
|
||||
public CommonResult<?> maxUploadSizeExceededExceptionHandler(MaxUploadSizeExceededException ex) {
|
||||
return CommonResult.error(BAD_REQUEST.getCode(), "上传文件过大,请调整后重试");
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 SpringMVC 请求地址不存在
|
||||
*
|
||||
@ -252,6 +266,16 @@ public class GlobalExceptionHandler {
|
||||
return CommonResult.error(FORBIDDEN);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 Guava UncheckedExecutionException
|
||||
*
|
||||
* 例如说,缓存加载报错,可见 <a href="https://t.zsxq.com/UszdH">https://t.zsxq.com/UszdH</a>
|
||||
*/
|
||||
@ExceptionHandler(value = UncheckedExecutionException.class)
|
||||
public CommonResult<?> uncheckedExecutionExceptionHandler(HttpServletRequest req, UncheckedExecutionException ex) {
|
||||
return allExceptionHandler(req, ex.getCause());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理业务异常 ServiceException
|
||||
*
|
||||
@ -282,6 +306,12 @@ public class GlobalExceptionHandler {
|
||||
*/
|
||||
@ExceptionHandler(value = Exception.class)
|
||||
public CommonResult<?> defaultExceptionHandler(HttpServletRequest req, Throwable ex) {
|
||||
// 特殊:如果是 ServiceException 的异常,则直接返回
|
||||
// 例如说:https://gitee.com/zhijiantianya/yudao-cloud/issues/ICSSRM、https://gitee.com/zhijiantianya/yudao-cloud/issues/ICT6FM
|
||||
if (ex.getCause() != null && ex.getCause() instanceof ServiceException) {
|
||||
return serviceExceptionHandler((ServiceException) ex.getCause());
|
||||
}
|
||||
|
||||
// 情况一:处理表不存在的异常
|
||||
CommonResult<?> tableNotExistsResult = handleTableNotExists(ex);
|
||||
if (tableNotExistsResult != null) {
|
||||
|
||||
@ -145,6 +145,7 @@ public class WebFrameworkUtils {
|
||||
return (CommonResult<?>) request.getAttribute(REQUEST_ATTRIBUTE_COMMON_RESULT);
|
||||
}
|
||||
|
||||
@SuppressWarnings("PatternVariableCanBeUsed")
|
||||
public static HttpServletRequest getRequest() {
|
||||
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
|
||||
if (!(requestAttributes instanceof ServletRequestAttributes)) {
|
||||
|
||||
@ -3,4 +3,5 @@ cn.iocoder.yudao.framework.jackson.config.YudaoJacksonAutoConfiguration
|
||||
cn.iocoder.yudao.framework.swagger.config.YudaoSwaggerAutoConfiguration
|
||||
cn.iocoder.yudao.framework.web.config.YudaoWebAutoConfiguration
|
||||
cn.iocoder.yudao.framework.xss.config.YudaoXssAutoConfiguration
|
||||
cn.iocoder.yudao.framework.banner.config.YudaoBannerAutoConfiguration
|
||||
cn.iocoder.yudao.framework.banner.config.YudaoBannerAutoConfiguration
|
||||
cn.iocoder.yudao.framework.encrypt.config.YudaoApiEncryptAutoConfiguration
|
||||
@ -0,0 +1,86 @@
|
||||
package cn.iocoder.yudao.framework.encrypt;
|
||||
|
||||
import cn.hutool.core.util.RandomUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.crypto.SecureUtil;
|
||||
import cn.hutool.crypto.asymmetric.AsymmetricAlgorithm;
|
||||
import cn.hutool.crypto.asymmetric.KeyType;
|
||||
import cn.hutool.crypto.asymmetric.RSA;
|
||||
import cn.hutool.crypto.symmetric.SymmetricAlgorithm;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 各种 API 加解密的测试类:不是单测,而是方便大家生成密钥、加密、解密等操作。
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@SuppressWarnings("ConstantValue")
|
||||
public class ApiEncryptTest {
|
||||
|
||||
@Test
|
||||
public void testGenerateAsymmetric() {
|
||||
String asymmetricAlgorithm = AsymmetricAlgorithm.RSA.getValue();
|
||||
// String asymmetricAlgorithm = "SM2";
|
||||
// String asymmetricAlgorithm = SM4.ALGORITHM_NAME;
|
||||
// String asymmetricAlgorithm = SymmetricAlgorithm.AES.getValue();
|
||||
String requestClientKey = null;
|
||||
String requestServerKey = null;
|
||||
String responseClientKey = null;
|
||||
String responseServerKey = null;
|
||||
if (Objects.equals(asymmetricAlgorithm, AsymmetricAlgorithm.RSA.getValue())) {
|
||||
// 请求的密钥
|
||||
RSA requestRsa = SecureUtil.rsa();
|
||||
requestClientKey = requestRsa.getPublicKeyBase64();
|
||||
requestServerKey = requestRsa.getPrivateKeyBase64();
|
||||
// 响应的密钥
|
||||
RSA responseRsa = new RSA();
|
||||
responseClientKey = responseRsa.getPrivateKeyBase64();
|
||||
responseServerKey = responseRsa.getPublicKeyBase64();
|
||||
} else if (Objects.equals(asymmetricAlgorithm, SymmetricAlgorithm.AES.getValue())) {
|
||||
// AES 密钥可选 32、24、16 位
|
||||
// 请求的密钥(前后端密钥一致)
|
||||
requestClientKey = RandomUtil.randomNumbers(32);
|
||||
requestServerKey = requestClientKey;
|
||||
// 响应的密钥(前后端密钥一致)
|
||||
responseClientKey = RandomUtil.randomNumbers(32);
|
||||
responseServerKey = responseClientKey;
|
||||
}
|
||||
|
||||
// 打印结果
|
||||
System.out.println("requestClientKey = " + requestClientKey);
|
||||
System.out.println("requestServerKey = " + requestServerKey);
|
||||
System.out.println("responseClientKey = " + responseClientKey);
|
||||
System.out.println("responseServerKey = " + responseServerKey);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEncrypt_aes() {
|
||||
String key = "52549111389893486934626385991395";
|
||||
String body = "{\n" +
|
||||
" \"username\": \"admin\",\n" +
|
||||
" \"password\": \"admin123\",\n" +
|
||||
" \"uuid\": \"3acd87a09a4f48fb9118333780e94883\",\n" +
|
||||
" \"code\": \"1024\"\n" +
|
||||
"}";
|
||||
String encrypt = SecureUtil.aes(StrUtil.utf8Bytes(key))
|
||||
.encryptBase64(body);
|
||||
System.out.println("encrypt = " + encrypt);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEncrypt_rsa() {
|
||||
String key = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCls2rIpnGdYnLFgz1XU13GbNQ5DloyPpvW00FPGjqn5Z6JpK+kDtVlnkhwR87iRrE5Vf2WNqRX6vzbLSgveIQY8e8oqGCb829myjf1MuI+ZzN4ghf/7tEYhZJGPI9AbfxFqBUzm+kR3/HByAI22GLT96WM26QiMK8n3tIP/yiLswIDAQAB";
|
||||
String body = "{\n" +
|
||||
" \"username\": \"admin\",\n" +
|
||||
" \"password\": \"admin123\",\n" +
|
||||
" \"uuid\": \"3acd87a09a4f48fb9118333780e94883\",\n" +
|
||||
" \"code\": \"1024\"\n" +
|
||||
"}";
|
||||
String encrypt = SecureUtil.rsa(null, key)
|
||||
.encryptBase64(body, KeyType.PublicKey);
|
||||
System.out.println("encrypt = " + encrypt);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,46 @@
|
||||
package cn.iocoder.yudao.module.ai.framework.ai.core.model.gemini;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.ai.chat.model.ChatModel;
|
||||
import org.springframework.ai.chat.model.ChatResponse;
|
||||
import org.springframework.ai.chat.prompt.ChatOptions;
|
||||
import org.springframework.ai.chat.prompt.Prompt;
|
||||
import org.springframework.ai.openai.OpenAiChatModel;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
/**
|
||||
* 谷歌 Gemini {@link ChatModel} 实现类,基于 Google AI Studio 提供的 <a href="https://ai.google.dev/gemini-api/docs/openai">OpenAI 兼容方案</a>
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class GeminiChatModel implements ChatModel {
|
||||
|
||||
public static final String BASE_URL = "https://generativelanguage.googleapis.com/v1beta/openai/";
|
||||
public static final String COMPLETE_PATH = "/chat/completions";
|
||||
|
||||
public static final String MODEL_DEFAULT = "gemini-2.5-flash";
|
||||
|
||||
/**
|
||||
* 兼容 OpenAI 接口,进行复用
|
||||
*/
|
||||
private final OpenAiChatModel openAiChatModel;
|
||||
|
||||
@Override
|
||||
public ChatResponse call(Prompt prompt) {
|
||||
return openAiChatModel.call(prompt);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<ChatResponse> stream(Prompt prompt) {
|
||||
return openAiChatModel.stream(prompt);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChatOptions getDefaultOptions() {
|
||||
return openAiChatModel.getDefaultOptions();
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
package cn.iocoder.yudao.module.ai.framework.ai.core.webserch;
|
||||
|
||||
/**
|
||||
* 网络搜索客户端接口
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public interface AiWebSearchClient {
|
||||
|
||||
/**
|
||||
* 网页搜索
|
||||
*
|
||||
* @param request 搜索请求
|
||||
* @return 搜索结果
|
||||
*/
|
||||
AiWebSearchResponse search(AiWebSearchRequest request);
|
||||
|
||||
}
|
||||
@ -0,0 +1,34 @@
|
||||
package cn.iocoder.yudao.module.ai.framework.ai.core.webserch;
|
||||
|
||||
import jakarta.validation.constraints.Max;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class AiWebSearchRequest {
|
||||
|
||||
/**
|
||||
* 用户的搜索词
|
||||
*/
|
||||
@NotEmpty(message = "搜索词不能为空")
|
||||
private String query;
|
||||
|
||||
/**
|
||||
* 是否显示文本摘要
|
||||
*
|
||||
* true - 显示
|
||||
* false - 不显示(默认)
|
||||
*/
|
||||
private Boolean summary;
|
||||
|
||||
/**
|
||||
* 返回结果的条数
|
||||
*/
|
||||
@NotNull(message = "返回结果条数不能为空")
|
||||
@Min(message = "返回结果条数最小为 1", value = 1)
|
||||
@Max(message = "返回结果条数最大为 50", value = 50)
|
||||
private Integer count;
|
||||
|
||||
}
|
||||
@ -0,0 +1,62 @@
|
||||
package cn.iocoder.yudao.module.ai.framework.ai.core.webserch;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class AiWebSearchResponse {
|
||||
|
||||
/**
|
||||
* 总数(总共匹配的网页数)
|
||||
*/
|
||||
private Long total;
|
||||
|
||||
/**
|
||||
* 数据列表
|
||||
*/
|
||||
private List<WebPage> lists;
|
||||
|
||||
/**
|
||||
* 网页对象
|
||||
*/
|
||||
@Data
|
||||
public static class WebPage {
|
||||
|
||||
/**
|
||||
* 名称
|
||||
*
|
||||
* 例如说:搜狐网
|
||||
*/
|
||||
private String name;
|
||||
/**
|
||||
* 图标
|
||||
*/
|
||||
private String icon;
|
||||
|
||||
/**
|
||||
* 标题
|
||||
*
|
||||
* 例如说:186页|阿里巴巴:2024年环境、社会和治理(ESG)报告
|
||||
*/
|
||||
private String title;
|
||||
/**
|
||||
* URL
|
||||
*
|
||||
* 例如说:https://m.sohu.com/a/815036254_121819701/?pvid=000115_3w_a
|
||||
*/
|
||||
@SuppressWarnings("JavadocLinkAsPlainText")
|
||||
private String url;
|
||||
|
||||
/**
|
||||
* 内容的简短描述
|
||||
*/
|
||||
private String snippet;
|
||||
/**
|
||||
* 内容的文本摘要
|
||||
*/
|
||||
private String summary;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,153 @@
|
||||
package cn.iocoder.yudao.module.ai.framework.ai.core.webserch.bocha;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchClient;
|
||||
import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchRequest;
|
||||
import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchResponse;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.http.HttpStatusCode;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.reactive.function.client.ClientResponse;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
|
||||
|
||||
/**
|
||||
* 博查 {@link AiWebSearchClient} 实现类
|
||||
*
|
||||
* @see <a href="https://open.bochaai.com/overview">博查 AI 开放平台</a>
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
public class AiBoChaWebSearchClient implements AiWebSearchClient {
|
||||
|
||||
public static final String BASE_URL = "https://api.bochaai.com";
|
||||
private static final String AUTHORIZATION_HEADER = "Authorization";
|
||||
private static final String BEARER_PREFIX = "Bearer ";
|
||||
|
||||
private final WebClient webClient;
|
||||
|
||||
private final Predicate<HttpStatusCode> STATUS_PREDICATE = status -> !status.is2xxSuccessful();
|
||||
|
||||
private final Function<Object, Function<ClientResponse, Mono<? extends Throwable>>> EXCEPTION_FUNCTION =
|
||||
reqParam -> response -> response.bodyToMono(String.class).handle((responseBody, sink) -> {
|
||||
log.error("[AiBoChaWebSearchClient] 调用失败!请求参数:[{}],响应数据: [{}]", reqParam, responseBody);
|
||||
sink.error(new IllegalStateException("[AiBoChaWebSearchClient] 调用失败!"));
|
||||
});
|
||||
|
||||
public AiBoChaWebSearchClient(String apiKey) {
|
||||
this.webClient = WebClient.builder()
|
||||
.baseUrl(BASE_URL)
|
||||
.defaultHeaders((headers) -> {
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
headers.add(AUTHORIZATION_HEADER, BEARER_PREFIX + apiKey);
|
||||
})
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiWebSearchResponse search(AiWebSearchRequest request) {
|
||||
// 转换请求参数
|
||||
WebSearchRequest webSearchRequest = new WebSearchRequest(
|
||||
request.getQuery(),
|
||||
request.getSummary(),
|
||||
request.getCount()
|
||||
);
|
||||
// 调用博查 API
|
||||
CommonResult<WebSearchResponse> response = this.webClient.post()
|
||||
.uri("/v1/web-search")
|
||||
.bodyValue(webSearchRequest)
|
||||
.retrieve()
|
||||
.onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION.apply(webSearchRequest))
|
||||
.bodyToMono(new ParameterizedTypeReference<CommonResult<WebSearchResponse>>() {})
|
||||
.block();
|
||||
if (response == null) {
|
||||
throw new IllegalStateException("[search][搜索结果为空]");
|
||||
}
|
||||
if (response.getData() == null) {
|
||||
throw new IllegalStateException(String.format("[search][搜索失败,code = %s, msg = %s]",
|
||||
response.getCode(), response.getMsg()));
|
||||
}
|
||||
WebSearchResponse data = response.getData();
|
||||
|
||||
// 转换结果
|
||||
AiWebSearchResponse result = new AiWebSearchResponse();
|
||||
if (data.webPages() == null || CollUtil.isEmpty(data.webPages().value())) {
|
||||
return result.setTotal(0L).setLists(List.of());
|
||||
}
|
||||
return result.setTotal(data.webPages().totalEstimatedMatches())
|
||||
.setLists(convertList(data.webPages().value(), page -> new AiWebSearchResponse.WebPage()
|
||||
.setName(page.siteName()).setIcon(page.siteIcon())
|
||||
.setTitle(page.name()).setUrl(page.url())
|
||||
.setSnippet(page.snippet()).setSummary(page.summary())));
|
||||
}
|
||||
|
||||
/**
|
||||
* 网页搜索请求参数
|
||||
*/
|
||||
@JsonInclude(value = JsonInclude.Include.NON_NULL)
|
||||
public record WebSearchRequest(
|
||||
String query,
|
||||
Boolean summary,
|
||||
Integer count
|
||||
) {
|
||||
public WebSearchRequest {
|
||||
Assert.notBlank(query, "query 不能为空");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 网页搜索响应
|
||||
*/
|
||||
@JsonInclude(value = JsonInclude.Include.NON_NULL)
|
||||
public record WebSearchResponse(
|
||||
WebSearchWebPages webPages
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 网页搜索结果
|
||||
*/
|
||||
@JsonInclude(value = JsonInclude.Include.NON_NULL)
|
||||
public record WebSearchWebPages(
|
||||
String webSearchUrl,
|
||||
Long totalEstimatedMatches,
|
||||
List<WebPageValue> value,
|
||||
Boolean someResultsRemoved
|
||||
) {
|
||||
|
||||
/**
|
||||
* 网页结果值
|
||||
*/
|
||||
@JsonInclude(value = JsonInclude.Include.NON_NULL)
|
||||
public record WebPageValue(
|
||||
String id,
|
||||
String name,
|
||||
String url,
|
||||
String displayUrl,
|
||||
String snippet,
|
||||
String summary,
|
||||
String siteName,
|
||||
String siteIcon,
|
||||
String datePublished,
|
||||
String dateLastCrawled,
|
||||
String cachedPageUrl,
|
||||
String language,
|
||||
Boolean isFamilyFriendly,
|
||||
Boolean isNavigational
|
||||
) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 占位
|
||||
*/
|
||||
package cn.iocoder.yudao.module.ai.framework.security.core;
|
||||
@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 参考 <a href="https://docs.spring.io/spring-ai/reference/api/tools.html#_methods_as_tools">Tool Calling —— Methods as Tools</a>
|
||||
*/
|
||||
package cn.iocoder.yudao.module.ai.tool.function;
|
||||
@ -0,0 +1,19 @@
|
||||
package cn.iocoder.yudao.module.ai.tool.method;
|
||||
|
||||
/**
|
||||
* 来自 Spring AI 官方文档
|
||||
*
|
||||
* Represents a person with basic information.
|
||||
* This is an immutable record.
|
||||
*/
|
||||
public record Person(
|
||||
int id,
|
||||
String firstName,
|
||||
String lastName,
|
||||
String email,
|
||||
String sex,
|
||||
String ipAddress,
|
||||
String jobTitle,
|
||||
int age
|
||||
) {
|
||||
}
|
||||
@ -0,0 +1,80 @@
|
||||
package cn.iocoder.yudao.module.ai.tool.method;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 来自 Spring AI 官方文档
|
||||
*
|
||||
* Service interface for managing Person data.
|
||||
* Defines the contract for CRUD operations and search/filter functionalities.
|
||||
*/
|
||||
public interface PersonService {
|
||||
|
||||
/**
|
||||
* Creates a new Person record.
|
||||
* Assigns a unique ID to the person and stores it.
|
||||
*
|
||||
* @param personData The data for the new person (ID field is ignored). Must not be null.
|
||||
* @return The created Person record, including the generated ID.
|
||||
*/
|
||||
Person createPerson(Person personData);
|
||||
|
||||
/**
|
||||
* Retrieves a Person by their unique ID.
|
||||
*
|
||||
* @param id The ID of the person to retrieve.
|
||||
* @return An Optional containing the found Person, or an empty Optional if not found.
|
||||
*/
|
||||
Optional<Person> getPersonById(int id);
|
||||
|
||||
/**
|
||||
* Retrieves all Person records currently stored.
|
||||
*
|
||||
* @return An unmodifiable List containing all Persons. Returns an empty list if none exist.
|
||||
*/
|
||||
List<Person> getAllPersons();
|
||||
|
||||
/**
|
||||
* Updates an existing Person record identified by ID.
|
||||
* Replaces the existing data with the provided data, keeping the original ID.
|
||||
*
|
||||
* @param id The ID of the person to update.
|
||||
* @param updatedPersonData The new data for the person (ID field is ignored). Must not be null.
|
||||
* @return true if the person was found and updated, false otherwise.
|
||||
*/
|
||||
boolean updatePerson(int id, Person updatedPersonData);
|
||||
|
||||
/**
|
||||
* Deletes a Person record identified by ID.
|
||||
*
|
||||
* @param id The ID of the person to delete.
|
||||
* @return true if the person was found and deleted, false otherwise.
|
||||
*/
|
||||
boolean deletePerson(int id);
|
||||
|
||||
/**
|
||||
* Searches for Persons whose job title contains the given query string (case-insensitive).
|
||||
*
|
||||
* @param jobTitleQuery The string to search for within job titles. Can be null or blank.
|
||||
* @return An unmodifiable List of matching Persons. Returns an empty list if no matches or query is invalid.
|
||||
*/
|
||||
List<Person> searchByJobTitle(String jobTitleQuery);
|
||||
|
||||
/**
|
||||
* Filters Persons by their exact sex (case-insensitive).
|
||||
*
|
||||
* @param sex The sex to filter by (e.g., "Male", "Female"). Can be null or blank.
|
||||
* @return An unmodifiable List of matching Persons. Returns an empty list if no matches or filter is invalid.
|
||||
*/
|
||||
List<Person> filterBySex(String sex);
|
||||
|
||||
/**
|
||||
* Filters Persons by their exact age.
|
||||
*
|
||||
* @param age The age to filter by.
|
||||
* @return An unmodifiable List of matching Persons. Returns an empty list if no matches.
|
||||
*/
|
||||
List<Person> filterByAge(int age);
|
||||
|
||||
}
|
||||
@ -0,0 +1,336 @@
|
||||
package cn.iocoder.yudao.module.ai.tool.method;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.ai.tool.annotation.Tool;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* 来自 Spring AI 官方文档
|
||||
*
|
||||
* Implementation of the PersonService interface using an in-memory data store.
|
||||
* Manages a collection of Person objects loaded from embedded CSV data.
|
||||
* This class is thread-safe due to the use of ConcurrentHashMap and AtomicInteger.
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class PersonServiceImpl implements PersonService {
|
||||
|
||||
private final Map<Integer, Person> personStore = new ConcurrentHashMap<>();
|
||||
|
||||
private AtomicInteger idGenerator;
|
||||
|
||||
/**
|
||||
* Embedded CSV data for initial population
|
||||
*/
|
||||
private static final String CSV_DATA = """
|
||||
Id,FirstName,LastName,Email,Sex,IpAddress,JobTitle,Age
|
||||
1,Fons,Tollfree,ftollfree0@senate.gov,Male,55.1 Tollfree Lane,Research Associate,31
|
||||
2,Emlynne,Tabourier,etabourier1@networksolutions.com,Female,18 Tabourier Way,Associate Professor,38
|
||||
3,Shae,Johncey,sjohncey2@yellowpages.com,Male,1 Johncey Circle,Structural Analysis Engineer,30
|
||||
4,Sebastien,Bradly,sbradly3@mapquest.com,Male,2 Bradly Hill,Chief Executive Officer,40
|
||||
5,Harriott,Kitteringham,hkitteringham4@typepad.com,Female,3 Kitteringham Drive,VP Sales,47
|
||||
6,Anallise,Parradine,aparradine5@miibeian.gov.cn,Female,4 Parradine Street,Analog Circuit Design manager,44
|
||||
7,Gorden,Kirkbright,gkirkbright6@reuters.com,Male,5 Kirkbright Plaza,Senior Editor,40
|
||||
8,Veradis,Ledwitch,vledwitch7@google.com.au,Female,6 Ledwitch Avenue,Computer Systems Analyst IV,44
|
||||
9,Agnesse,Penhalurick,apenhalurick8@google.it,Female,7 Penhalurick Terrace,Automation Specialist IV,41
|
||||
10,Bibby,Hutable,bhutable9@craigslist.org,Female,8 Hutable Place,Account Representative I,43
|
||||
11,Karoly,Lightoller,klightollera@rakuten.co.jp,Female,9 Lightoller Parkway,Senior Developer,46
|
||||
12,Cristine,Durrad,cdurradb@aol.com,Female,10 Durrad Center,Senior Developer,48
|
||||
13,Aggy,Napier,anapierc@hostgator.com,Female,11 Napier Court,VP Product Management,44
|
||||
14,Prisca,Caddens,pcaddensd@vinaora.com,Female,12 Caddens Alley,Business Systems Development Analyst,41
|
||||
15,Khalil,McKernan,kmckernane@google.fr,Male,13 McKernan Pass,Engineer IV,44
|
||||
16,Lorry,MacTrusty,lmactrustyf@eventbrite.com,Male,14 MacTrusty Junction,Design Engineer,42
|
||||
17,Casandra,Worsell,cworsellg@goo.gl,Female,15 Worsell Point,Systems Administrator IV,45
|
||||
18,Ulrikaumeko,Haveline,uhavelineh@usgs.gov,Female,16 Haveline Trail,Financial Advisor,42
|
||||
19,Shurlocke,Albany,salbanyi@artisteer.com,Male,17 Albany Plaza,Software Test Engineer III,46
|
||||
20,Myrilla,Brimilcombe,mbrimilcombej@accuweather.com,Female,18 Brimilcombe Road,Programmer Analyst I,48
|
||||
21,Carlina,Scimonelli,cscimonellik@va.gov,Female,19 Scimonelli Pass,Help Desk Technician,45
|
||||
22,Tina,Goullee,tgoulleel@miibeian.gov.cn,Female,20 Goullee Crossing,Accountant IV,43
|
||||
23,Adriaens,Storek,astorekm@devhub.com,Female,21 Storek Avenue,Recruiting Manager,40
|
||||
24,Tedra,Giraudot,tgiraudotn@wiley.com,Female,22 Giraudot Terrace,Speech Pathologist,47
|
||||
25,Josiah,Soares,jsoareso@google.nl,Male,23 Soares Street,Tax Accountant,45
|
||||
26,Kayle,Gaukrodge,kgaukrodgep@wikispaces.com,Female,24 Gaukrodge Parkway,Accountant II,43
|
||||
27,Ardys,Chuter,achuterq@ustream.tv,Female,25 Chuter Drive,Engineer IV,41
|
||||
28,Francyne,Baudinet,fbaudinetr@newyorker.com,Female,26 Baudinet Center,VP Accounting,48
|
||||
29,Gerick,Bullan,gbullans@seesaa.net,Male,27 Bullan Way,Senior Financial Analyst,43
|
||||
30,Northrup,Grivori,ngrivorit@unc.edu,Male,28 Grivori Plaza,Systems Administrator I,45
|
||||
31,Town,Duguid,tduguidu@squarespace.com,Male,29 Duguid Pass,Safety Technician IV,46
|
||||
32,Pierette,Kopisch,pkopischv@google.com.br,Female,30 Kopisch Lane,Director of Sales,41
|
||||
33,Jacquenetta,Le Prevost,jleprevostw@netlog.com,Female,31 Le Prevost Trail,Senior Developer,47
|
||||
34,Garvy,Rusted,grustedx@aboutads.info,Male,32 Rusted Junction,Senior Developer,42
|
||||
35,Clarice,Aysh,cayshy@merriam-webster.com,Female,33 Aysh Avenue,VP Quality Control,40
|
||||
36,Tracie,Fedorski,tfedorskiz@bloglines.com,Male,34 Fedorski Terrace,Design Engineer,44
|
||||
37,Noelyn,Matushenko,nmatushenko10@globo.com,Female,35 Matushenko Place,VP Sales,48
|
||||
38,Rudiger,Klaesson,rklaesson11@usnews.com,Male,36 Klaesson Road,Database Administrator IV,43
|
||||
39,Mirella,Syddie,msyddie12@geocities.jp,Female,37 Syddie Circle,Geological Engineer,46
|
||||
40,Donalt,O'Lunny,dolunny13@elpais.com,Male,38 O'Lunny Center,Analog Circuit Design manager,41
|
||||
41,Guntar,Deniskevich,gdeniskevich14@google.com.hk,Male,39 Deniskevich Way,Structural Engineer,47
|
||||
42,Hort,Shufflebotham,hshufflebotham15@about.me,Male,40 Shufflebotham Court,Structural Analysis Engineer,45
|
||||
43,Dominique,Thickett,dthickett16@slashdot.org,Male,41 Thickett Crossing,Safety Technician I,42
|
||||
44,Zebulen,Piscopello,zpiscopello17@umich.edu,Male,42 Piscopello Parkway,Web Developer II,40
|
||||
45,Mellicent,Mac Giany,mmacgiany18@state.tx.us,Female,43 Mac Giany Pass,Assistant Manager,44
|
||||
46,Merle,Bounds,mbounds19@amazon.co.jp,Female,44 Bounds Alley,Systems Administrator III,41
|
||||
47,Madelle,Farbrace,mfarbrace1a@xinhuanet.com,Female,45 Farbrace Terrace,Quality Engineer,48
|
||||
48,Galvin,O'Sheeryne,gosheeryne1b@addtoany.com,Male,46 O'Sheeryne Way,Environmental Specialist,43
|
||||
49,Guillemette,Bootherstone,gbootherstone1c@nationalgeographic.com,Female,47 Bootherstone Plaza,Professor,46
|
||||
50,Letti,Aylmore,laylmore1d@vinaora.com,Female,48 Aylmore Circle,Automation Specialist I,40
|
||||
51,Nonie,Rivalland,nrivalland1e@weather.com,Female,49 Rivalland Avenue,Software Test Engineer IV,45
|
||||
52,Jacquelynn,Halfacre,jhalfacre1f@surveymonkey.com,Female,50 Halfacre Pass,Geologist II,42
|
||||
53,Anderea,MacKibbon,amackibbon1g@weibo.com,Female,51 MacKibbon Parkway,Automation Specialist II,47
|
||||
54,Wash,Klimko,wklimko1h@slashdot.org,Male,52 Klimko Alley,Database Administrator I,40
|
||||
55,Flori,Kynett,fkynett1i@auda.org.au,Female,53 Kynett Trail,Quality Control Specialist,46
|
||||
56,Libbey,Penswick,lpenswick1j@google.co.uk,Female,54 Penswick Point,VP Accounting,43
|
||||
57,Silvanus,Skellorne,sskellorne1k@booking.com,Male,55 Skellorne Drive,Account Executive,48
|
||||
58,Carmine,Mateos,cmateos1l@plala.or.jp,Male,56 Mateos Terrace,Systems Administrator I,41
|
||||
59,Sheffie,Blazewicz,sblazewicz1m@google.com.au,Male,57 Blazewicz Center,VP Sales,44
|
||||
60,Leanor,Worsnop,lworsnop1n@uol.com.br,Female,58 Worsnop Plaza,Systems Administrator III,45
|
||||
61,Caspar,Pamment,cpamment1o@google.co.jp,Male,59 Pamment Court,Senior Financial Analyst,42
|
||||
62,Justinian,Pentycost,jpentycost1p@sciencedaily.com,Male,60 Pentycost Way,Senior Quality Engineer,47
|
||||
63,Gerianne,Jarnell,gjarnell1q@bing.com,Female,61 Jarnell Avenue,Help Desk Operator,40
|
||||
64,Boycie,Zanetto,bzanetto1r@about.com,Male,62 Zanetto Place,Quality Engineer,46
|
||||
65,Camilla,Mac Giany,cmacgiany1s@state.gov,Female,63 Mac Giany Parkway,Senior Cost Accountant,43
|
||||
66,Hadlee,Piscopiello,hpiscopiello1t@artisteer.com,Male,64 Piscopiello Street,Account Representative III,48
|
||||
67,Bobbie,Penvarden,bpenvarden1u@google.cn,Male,65 Penvarden Lane,Help Desk Operator,41
|
||||
68,Ali,Gowlett,agowlett1v@parallels.com,Male,66 Gowlett Pass,VP Marketing,44
|
||||
69,Olivette,Acome,oacome1w@qq.com,Female,67 Acome Hill,VP Product Management,45
|
||||
70,Jehanna,Brotherheed,jbrotherheed1x@google.nl,Female,68 Brotherheed Junction,Database Administrator III,42
|
||||
71,Morgan,Berthomieu,mberthomieu1y@artisteer.com,Male,69 Berthomieu Alley,Systems Administrator II,47
|
||||
72,Linzy,Shilladay,lshilladay1z@icq.com,Female,70 Shilladay Trail,Research Assistant IV,40
|
||||
73,Faydra,Brimner,fbrimner20@mozilla.org,Female,71 Brimner Road,Senior Editor,46
|
||||
74,Gwenore,Oxlee,goxlee21@devhub.com,Female,72 Oxlee Terrace,Systems Administrator II,43
|
||||
75,Evangelin,Beinke,ebeinke22@mozilla.com,Female,73 Beinke Circle,Accountant I,48
|
||||
76,Missy,Cockling,mcockling23@si.edu,Female,74 Cockling Way,Software Engineer I,41
|
||||
77,Suzanne,Klimschak,sklimschak24@etsy.com,Female,75 Klimschak Plaza,Tax Accountant,44
|
||||
78,Candide,Goricke,cgoricke25@weebly.com,Female,76 Goricke Pass,Sales Associate,45
|
||||
79,Gerome,Pinsent,gpinsent26@google.com.au,Male,77 Pinsent Junction,Software Consultant,42
|
||||
80,Lezley,Mac Giany,lmacgiany27@scribd.com,Male,78 Mac Giany Alley,Operator,47
|
||||
81,Tobiah,Durn,tdurn28@state.tx.us,Male,79 Durn Court,VP Sales,40
|
||||
82,Sherlocke,Cockshoot,scockshoot29@yelp.com,Male,80 Cockshoot Street,Senior Financial Analyst,46
|
||||
83,Myrle,Speenden,mspeenden2a@utexas.edu,Female,81 Speenden Center,Senior Developer,43
|
||||
84,Isidore,Gorries,igorries2b@flavors.me,Male,82 Gorries Parkway,Sales Representative,48
|
||||
85,Isac,Kitchingman,ikitchingman2c@businessinsider.com,Male,83 Kitchingman Drive,VP Accounting,41
|
||||
86,Benedetta,Purrier,bpurrier2d@admin.ch,Female,84 Purrier Trail,VP Accounting,44
|
||||
87,Tera,Fitchell,tfitchell2e@fotki.com,Female,85 Fitchell Place,Software Engineer IV,45
|
||||
88,Abbe,Pamment,apamment2f@about.com,Male,86 Pamment Avenue,VP Sales,42
|
||||
89,Jandy,Gommowe,jgommowe2g@angelfire.com,Female,87 Gommowe Road,Financial Analyst,47
|
||||
90,Karena,Fussey,kfussey2h@google.com.au,Female,88 Fussey Point,Assistant Professor,40
|
||||
91,Gaspar,Pammenter,gpammenter2i@google.com.br,Male,89 Pammenter Hill,Help Desk Operator,46
|
||||
92,Stanwood,Mac Giany,smacgiany2j@prlog.org,Male,90 Mac Giany Terrace,Research Associate,43
|
||||
93,Byrom,Beedell,bbeedell2k@google.co.jp,Male,91 Beedell Way,VP Sales,48
|
||||
94,Annabella,Rowbottom,arowbottom2l@google.com.au,Female,92 Rowbottom Plaza,Help Desk Operator,41
|
||||
95,Rodolphe,Debell,rdebell2m@imageshack.us,Male,93 Debell Pass,Design Engineer,44
|
||||
96,Tyne,Gommey,tgommey2n@joomla.org,Female,94 Gommey Junction,VP Marketing,45
|
||||
97,Christoper,Pincked,cpincked2o@icq.com,Male,95 Pincked Alley,Human Resources Manager,42
|
||||
98,Kore,Le Prevost,kleprevost2p@tripadvisor.com,Female,96 Le Prevost Street,VP Quality Control,47
|
||||
99,Ceciley,Petrolli,cpetrolli2q@oaic.gov.au,Female,97 Petrolli Court,Senior Developer,40
|
||||
100,Elspeth,Mac Giany,emacgiany2r@icio.us,Female,98 Mac Giany Parkway,Internal Auditor,46
|
||||
""";
|
||||
|
||||
/**
|
||||
* Initializes the service after dependency injection by loading data from the CSV string.
|
||||
* Uses @PostConstruct to ensure this runs after the bean is created.
|
||||
*/
|
||||
@PostConstruct
|
||||
private void initializeData() {
|
||||
log.info("Initializing PersonService data store...");
|
||||
int maxId = loadDataFromCsv();
|
||||
idGenerator = new AtomicInteger(maxId);
|
||||
log.info("PersonService initialized with {} records. Next ID: {}", personStore.size(), idGenerator.get() + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the embedded CSV data and populates the in-memory store.
|
||||
* Calculates the maximum ID found in the data to initialize the ID generator.
|
||||
*
|
||||
* @return The maximum ID found in the loaded CSV data.
|
||||
*/
|
||||
private int loadDataFromCsv() {
|
||||
final AtomicInteger currentMaxId = new AtomicInteger(0);
|
||||
// Clear existing data before loading (important for tests or re-initialization scenarios)
|
||||
personStore.clear();
|
||||
try (Stream<String> lines = CSV_DATA.lines().skip(1)) { // Skip header row
|
||||
lines.forEach(line -> {
|
||||
try {
|
||||
// Split carefully, handling potential commas within quoted fields if necessary (simple split here)
|
||||
String[] fields = line.split(",", 8); // Limit split to handle potential commas in job title
|
||||
if (fields.length == 8) {
|
||||
int id = Integer.parseInt(fields[0].trim());
|
||||
String firstName = fields[1].trim();
|
||||
String lastName = fields[2].trim();
|
||||
String email = fields[3].trim();
|
||||
String sex = fields[4].trim();
|
||||
String ipAddress = fields[5].trim();
|
||||
String jobTitle = fields[6].trim();
|
||||
int age = Integer.parseInt(fields[7].trim());
|
||||
|
||||
Person person = new Person(id, firstName, lastName, email, sex, ipAddress, jobTitle, age);
|
||||
personStore.put(id, person);
|
||||
currentMaxId.updateAndGet(max -> Math.max(max, id));
|
||||
} else {
|
||||
log.warn("Skipping malformed CSV line (expected 8 fields, found {}): {}", fields.length, line);
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
log.warn("Skipping line due to parsing error (ID or Age): {} - Error: {}", line, e.getMessage());
|
||||
} catch (Exception e) {
|
||||
log.error("Skipping line due to unexpected error: {} - Error: {}", line, e.getMessage(), e);
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
log.error("Fatal error reading embedded CSV data: {}", e.getMessage(), e);
|
||||
// In a real application, might throw a specific initialization exception
|
||||
}
|
||||
return currentMaxId.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Tool(
|
||||
name = "ps_create_person",
|
||||
description = "Create a new person record in the in-memory store."
|
||||
)
|
||||
public Person createPerson(Person personData) {
|
||||
if (personData == null) {
|
||||
throw new IllegalArgumentException("Person data cannot be null");
|
||||
}
|
||||
int newId = idGenerator.incrementAndGet();
|
||||
// Create a new Person record using data from the input, but with the generated ID
|
||||
Person newPerson = new Person(
|
||||
newId,
|
||||
personData.firstName(),
|
||||
personData.lastName(),
|
||||
personData.email(),
|
||||
personData.sex(),
|
||||
personData.ipAddress(),
|
||||
personData.jobTitle(),
|
||||
personData.age()
|
||||
);
|
||||
personStore.put(newId, newPerson);
|
||||
log.debug("Created person: {}", newPerson);
|
||||
return newPerson;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Tool(
|
||||
name = "ps_get_person_by_id",
|
||||
description = "Retrieve a person record by ID from the in-memory store."
|
||||
)
|
||||
public Optional<Person> getPersonById(int id) {
|
||||
Person person = personStore.get(id);
|
||||
log.debug("Retrieved person by ID {}: {}", id, person);
|
||||
return Optional.ofNullable(person);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Tool(
|
||||
name = "ps_get_all_persons",
|
||||
description = "Retrieve all person records from the in-memory store."
|
||||
)
|
||||
public List<Person> getAllPersons() {
|
||||
// Return an unmodifiable view of the values
|
||||
List<Person> allPersons = personStore.values().stream().toList();
|
||||
log.debug("Retrieved all persons (count: {})", allPersons.size());
|
||||
return allPersons;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Tool(
|
||||
name = "ps_update_person",
|
||||
description = "Update an existing person record by ID in the in-memory store."
|
||||
)
|
||||
public boolean updatePerson(int id, Person updatedPersonData) {
|
||||
if (updatedPersonData == null) {
|
||||
throw new IllegalArgumentException("Updated person data cannot be null");
|
||||
}
|
||||
// Use computeIfPresent for atomic update if the key exists
|
||||
Person result = personStore.computeIfPresent(id, (key, existingPerson) ->
|
||||
// Create a new Person record with the original ID but updated data
|
||||
new Person(
|
||||
id, // Keep original ID
|
||||
updatedPersonData.firstName(),
|
||||
updatedPersonData.lastName(),
|
||||
updatedPersonData.email(),
|
||||
updatedPersonData.sex(),
|
||||
updatedPersonData.ipAddress(),
|
||||
updatedPersonData.jobTitle(),
|
||||
updatedPersonData.age()
|
||||
)
|
||||
);
|
||||
boolean updated = result != null;
|
||||
log.debug("Update attempt for ID {}: {}", id, updated ? "Successful" : "Failed (Not Found)");
|
||||
if(updated) log.trace("Updated person data for ID {}: {}", id, result);
|
||||
return updated;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Tool(
|
||||
name = "ps_delete_person",
|
||||
description = "Delete a person record by ID from the in-memory store."
|
||||
)
|
||||
public boolean deletePerson(int id) {
|
||||
boolean removed = personStore.remove(id) != null;
|
||||
log.debug("Delete attempt for ID {}: {}", id, removed ? "Successful" : "Failed (Not Found)");
|
||||
return removed;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Tool(
|
||||
name = "ps_search_by_job_title",
|
||||
description = "Search for persons by job title in the in-memory store."
|
||||
)
|
||||
public List<Person> searchByJobTitle(String jobTitleQuery) {
|
||||
if (jobTitleQuery == null || jobTitleQuery.isBlank()) {
|
||||
log.debug("Search by job title skipped due to blank query.");
|
||||
return Collections.emptyList();
|
||||
}
|
||||
String lowerCaseQuery = jobTitleQuery.toLowerCase();
|
||||
List<Person> results = personStore.values().stream()
|
||||
.filter(person -> person.jobTitle() != null && person.jobTitle().toLowerCase().contains(lowerCaseQuery))
|
||||
.collect(Collectors.toList());
|
||||
log.debug("Search by job title '{}' found {} results.", jobTitleQuery, results.size());
|
||||
return Collections.unmodifiableList(results);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Tool(
|
||||
name = "ps_filter_by_sex",
|
||||
description = "Filters Persons by sex (case-insensitive)."
|
||||
)
|
||||
public List<Person> filterBySex(String sex) {
|
||||
if (sex == null || sex.isBlank()) {
|
||||
log.debug("Filter by sex skipped due to blank filter.");
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<Person> results = personStore.values().stream()
|
||||
.filter(person -> person.sex() != null && person.sex().equalsIgnoreCase(sex))
|
||||
.collect(Collectors.toList());
|
||||
log.debug("Filter by sex '{}' found {} results.", sex, results.size());
|
||||
return Collections.unmodifiableList(results);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Tool(
|
||||
name = "ps_filter_by_age",
|
||||
description = "Filters Persons by age."
|
||||
)
|
||||
public List<Person> filterByAge(int age) {
|
||||
if (age < 0) {
|
||||
log.debug("Filter by age skipped due to negative age: {}", age);
|
||||
return Collections.emptyList(); // Or throw IllegalArgumentException based on requirements
|
||||
}
|
||||
List<Person> results = personStore.values().stream()
|
||||
.filter(person -> person.age() == age)
|
||||
.collect(Collectors.toList());
|
||||
log.debug("Filter by age {} found {} results.", age, results.size());
|
||||
return Collections.unmodifiableList(results);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 参考 <a href="https://docs.spring.io/spring-ai/reference/api/tools.html#_methods_as_tools">Tool Calling —— Methods as Tools</a>
|
||||
*/
|
||||
package cn.iocoder.yudao.module.ai.tool.method;
|
||||
@ -0,0 +1,37 @@
|
||||
package cn.iocoder.yudao.module.ai.util;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.tika.Tika;
|
||||
|
||||
/**
|
||||
* 文件类型 Utils
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
public class FileTypeUtils {
|
||||
|
||||
private static final Tika TIKA = new Tika();
|
||||
|
||||
/**
|
||||
* 已知文件名,获取文件类型,在某些情况下比通过字节数组准确,例如使用 jar 文件时,通过名字更为准确
|
||||
*
|
||||
* @param name 文件名
|
||||
* @return mineType 无法识别时会返回“application/octet-stream”
|
||||
*/
|
||||
public static String getMineType(String name) {
|
||||
return TIKA.detect(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否是图片
|
||||
*
|
||||
* @param mineType 类型
|
||||
* @return 是否是图片
|
||||
*/
|
||||
public static boolean isImage(String mineType) {
|
||||
return StrUtil.startWith(mineType, "image/");
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,87 @@
|
||||
package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat;
|
||||
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.ai.anthropic.AnthropicChatModel;
|
||||
import org.springframework.ai.anthropic.AnthropicChatOptions;
|
||||
import org.springframework.ai.anthropic.api.AnthropicApi;
|
||||
import org.springframework.ai.chat.messages.Message;
|
||||
import org.springframework.ai.chat.messages.SystemMessage;
|
||||
import org.springframework.ai.chat.messages.UserMessage;
|
||||
import org.springframework.ai.chat.model.ChatResponse;
|
||||
import org.springframework.ai.chat.prompt.Prompt;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* {@link AnthropicChatModel} 集成测试类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class AnthropicChatModelTest {
|
||||
|
||||
private final AnthropicChatModel chatModel = AnthropicChatModel.builder()
|
||||
.anthropicApi(AnthropicApi.builder()
|
||||
.apiKey("sk-muubv7cXeLw0Etgs743f365cD5Ea44429946Fa7e672d8942")
|
||||
.baseUrl("https://aihubmix.com")
|
||||
.build())
|
||||
.defaultOptions(AnthropicChatOptions.builder()
|
||||
.model(AnthropicApi.ChatModel.CLAUDE_SONNET_4)
|
||||
.temperature(0.7)
|
||||
.maxTokens(4096)
|
||||
.build())
|
||||
.build();
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
public void testCall() {
|
||||
// 准备参数
|
||||
List<Message> messages = new ArrayList<>();
|
||||
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
|
||||
messages.add(new UserMessage("1 + 1 = ?"));
|
||||
|
||||
// 调用
|
||||
ChatResponse response = chatModel.call(new Prompt(messages));
|
||||
// 打印结果
|
||||
System.out.println(response);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
public void testStream() {
|
||||
// 准备参数
|
||||
List<Message> messages = new ArrayList<>();
|
||||
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
|
||||
messages.add(new UserMessage("1 + 1 = ?"));
|
||||
|
||||
// 调用
|
||||
Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages));
|
||||
// 打印结果
|
||||
flux.doOnNext(System.out::println).then().block();
|
||||
}
|
||||
|
||||
// TODO @芋艿:需要等 spring ai 升级:https://github.com/spring-projects/spring-ai/pull/2800
|
||||
@Test
|
||||
@Disabled
|
||||
public void testStream_thinking() {
|
||||
// 准备参数
|
||||
List<Message> messages = new ArrayList<>();
|
||||
messages.add(new UserMessage("thkinking 下,1+1 为什么等于 2 "));
|
||||
AnthropicChatOptions options = AnthropicChatOptions.builder()
|
||||
.model(AnthropicApi.ChatModel.CLAUDE_SONNET_4)
|
||||
.thinking(AnthropicApi.ThinkingType.ENABLED, 3096)
|
||||
.temperature(1D)
|
||||
.build();
|
||||
|
||||
// 调用
|
||||
Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages, options));
|
||||
// 打印结果
|
||||
flux.doOnNext(response -> {
|
||||
// System.out.println(response);
|
||||
System.out.println(response.getResult());
|
||||
}).then().block();
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,68 @@
|
||||
package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat;
|
||||
|
||||
import cn.iocoder.yudao.module.ai.framework.ai.core.model.gemini.GeminiChatModel;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.ai.chat.messages.Message;
|
||||
import org.springframework.ai.chat.messages.SystemMessage;
|
||||
import org.springframework.ai.chat.messages.UserMessage;
|
||||
import org.springframework.ai.chat.model.ChatResponse;
|
||||
import org.springframework.ai.chat.prompt.Prompt;
|
||||
import org.springframework.ai.openai.OpenAiChatModel;
|
||||
import org.springframework.ai.openai.OpenAiChatOptions;
|
||||
import org.springframework.ai.openai.api.OpenAiApi;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* {@link GeminiChatModel} 集成测试
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class GeminiChatModelTests {
|
||||
|
||||
private final OpenAiChatModel openAiChatModel = OpenAiChatModel.builder()
|
||||
.openAiApi(OpenAiApi.builder()
|
||||
.baseUrl(GeminiChatModel.BASE_URL)
|
||||
.completionsPath(GeminiChatModel.COMPLETE_PATH)
|
||||
.apiKey("AIzaSyAVoBxgoFvvte820vEQMma2LKBnC98bqMQ")
|
||||
.build())
|
||||
.defaultOptions(OpenAiChatOptions.builder()
|
||||
.model(GeminiChatModel.MODEL_DEFAULT) // 模型
|
||||
.temperature(0.7)
|
||||
.build())
|
||||
.build();
|
||||
|
||||
private final GeminiChatModel chatModel = new GeminiChatModel(openAiChatModel);
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
public void testCall() {
|
||||
// 准备参数
|
||||
List<Message> messages = new ArrayList<>();
|
||||
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
|
||||
messages.add(new UserMessage("1 + 1 = ?"));
|
||||
|
||||
// 调用
|
||||
ChatResponse response = chatModel.call(new Prompt(messages));
|
||||
// 打印结果
|
||||
System.out.println(response);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
public void testStream() {
|
||||
// 准备参数
|
||||
List<Message> messages = new ArrayList<>();
|
||||
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
|
||||
messages.add(new UserMessage("1 + 1 = ?"));
|
||||
|
||||
// 调用
|
||||
Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages));
|
||||
// 打印结果
|
||||
flux.doOnNext(System.out::println).then().block();
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
package cn.iocoder.yudao.module.ai.framework.ai.core.websearch;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
||||
import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchRequest;
|
||||
import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchResponse;
|
||||
import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.bocha.AiBoChaWebSearchClient;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* {@link AiBoChaWebSearchClient} 集成测试类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class AiBoChaWebSearchClientTest {
|
||||
|
||||
private final AiBoChaWebSearchClient webSearchClient = new AiBoChaWebSearchClient(
|
||||
"sk-40500e52840f4d24b956d0b1d80d9abe");
|
||||
|
||||
@Test
|
||||
public void testSearch() {
|
||||
AiWebSearchRequest request = new AiWebSearchRequest()
|
||||
.setQuery("阿里巴巴")
|
||||
.setCount(3);
|
||||
AiWebSearchResponse response = webSearchClient.search(request);
|
||||
System.out.println(JsonUtils.toJsonPrettyString(response));
|
||||
}
|
||||
|
||||
}
|
||||
@ -42,4 +42,14 @@ public interface FileApi {
|
||||
String createFile(@NotEmpty(message = "文件内容不能为空") byte[] content,
|
||||
String name, String directory, String type);
|
||||
|
||||
/**
|
||||
* 生成文件预签名地址,用于读取
|
||||
*
|
||||
* @param url 完整的文件访问地址
|
||||
* @param expirationSeconds 访问有效期,单位秒
|
||||
* @return 文件预签名地址
|
||||
*/
|
||||
String presignGetUrl(@NotEmpty(message = "URL 不能为空") String url,
|
||||
Integer expirationSeconds);
|
||||
|
||||
}
|
||||
|
||||
@ -23,4 +23,9 @@ public class FileApiImpl implements FileApi {
|
||||
return fileService.createFile(content, name, directory, type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String presignGetUrl(String url, Integer expirationSeconds) {
|
||||
return fileService.presignGetUrl(url, expirationSeconds);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -43,7 +43,7 @@ public class FileController {
|
||||
|
||||
@PostMapping("/upload")
|
||||
@Operation(summary = "上传文件", description = "模式一:后端上传文件")
|
||||
public CommonResult<String> uploadFile(FileUploadReqVO uploadReqVO) throws Exception {
|
||||
public CommonResult<String> uploadFile(@Valid FileUploadReqVO uploadReqVO) throws Exception {
|
||||
MultipartFile file = uploadReqVO.getFile();
|
||||
byte[] content = IoUtil.readBytes(file.getInputStream());
|
||||
return success(fileService.createFile(content, file.getOriginalFilename(),
|
||||
@ -51,7 +51,7 @@ public class FileController {
|
||||
}
|
||||
|
||||
@GetMapping("/presigned-url")
|
||||
@Operation(summary = "获取文件预签名地址", description = "模式二:前端上传文件:用于前端直接上传七牛、阿里云 OSS 等文件存储器")
|
||||
@Operation(summary = "获取文件预签名地址(上传)", description = "模式二:前端上传文件:用于前端直接上传七牛、阿里云 OSS 等文件存储器")
|
||||
@Parameters({
|
||||
@Parameter(name = "name", description = "文件名称", required = true),
|
||||
@Parameter(name = "directory", description = "文件目录")
|
||||
@ -59,7 +59,7 @@ public class FileController {
|
||||
public CommonResult<FilePresignedUrlRespVO> getFilePresignedUrl(
|
||||
@RequestParam("name") String name,
|
||||
@RequestParam(value = "directory", required = false) String directory) {
|
||||
return success(fileService.getFilePresignedUrl(name, directory));
|
||||
return success(fileService.presignPutUrl(name, directory));
|
||||
}
|
||||
|
||||
@PostMapping("/create")
|
||||
|
||||
@ -42,7 +42,7 @@ public class AppFileController {
|
||||
}
|
||||
|
||||
@GetMapping("/presigned-url")
|
||||
@Operation(summary = "获取文件预签名地址", description = "模式二:前端上传文件:用于前端直接上传七牛、阿里云 OSS 等文件存储器")
|
||||
@Operation(summary = "获取文件预签名地址(上传)", description = "模式二:前端上传文件:用于前端直接上传七牛、阿里云 OSS 等文件存储器")
|
||||
@Parameters({
|
||||
@Parameter(name = "name", description = "文件名称", required = true),
|
||||
@Parameter(name = "directory", description = "文件目录")
|
||||
@ -50,7 +50,7 @@ public class AppFileController {
|
||||
public CommonResult<FilePresignedUrlRespVO> getFilePresignedUrl(
|
||||
@RequestParam("name") String name,
|
||||
@RequestParam(value = "directory", required = false) String directory) {
|
||||
return success(fileService.getFilePresignedUrl(name, directory));
|
||||
return success(fileService.presignPutUrl(name, directory));
|
||||
}
|
||||
|
||||
@PostMapping("/create")
|
||||
|
||||
@ -9,8 +9,6 @@ import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.generator.config.po.TableField;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
/**
|
||||
* 代码生成 column 字段定义
|
||||
@ -20,8 +18,6 @@ import lombok.experimental.Accessors;
|
||||
@TableName(value = "infra_codegen_column", autoResultMap = true)
|
||||
@KeySequence("infra_codegen_column_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TenantIgnore
|
||||
public class CodegenColumnDO extends BaseDO {
|
||||
|
||||
|
||||
@ -11,8 +11,6 @@ import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.generator.config.po.TableInfo;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
/**
|
||||
* 代码生成 table 表定义
|
||||
@ -22,8 +20,6 @@ import lombok.experimental.Accessors;
|
||||
@TableName(value = "infra_codegen_table", autoResultMap = true)
|
||||
@KeySequence("infra_codegen_table_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TenantIgnore
|
||||
public class CodegenTableDO extends BaseDO {
|
||||
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
package cn.iocoder.yudao.module.infra.framework.file.core.client;
|
||||
|
||||
import cn.iocoder.yudao.module.infra.framework.file.core.client.s3.FilePresignedUrlRespDTO;
|
||||
|
||||
/**
|
||||
* 文件客户端
|
||||
*
|
||||
@ -42,13 +40,26 @@ public interface FileClient {
|
||||
*/
|
||||
byte[] getContent(String path) throws Exception;
|
||||
|
||||
// ========== 文件签名,目前仅 S3 支持 ==========
|
||||
|
||||
/**
|
||||
* 获得文件预签名地址
|
||||
* 获得文件预签名地址,用于上传
|
||||
*
|
||||
* @param path 相对路径
|
||||
* @return 文件预签名地址
|
||||
*/
|
||||
default FilePresignedUrlRespDTO getPresignedObjectUrl(String path) throws Exception {
|
||||
default String presignPutUrl(String path) {
|
||||
throw new UnsupportedOperationException("不支持的操作");
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成文件预签名地址,用于读取
|
||||
*
|
||||
* @param url 完整的文件访问地址
|
||||
* @param expirationSeconds 访问有效期,单位秒
|
||||
* @return 文件预签名地址
|
||||
*/
|
||||
default String presignGetUrl(String url, Integer expirationSeconds) {
|
||||
throw new UnsupportedOperationException("不支持的操作");
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package cn.iocoder.yudao.module.infra.framework.file.core.client.local;
|
||||
|
||||
import cn.hutool.core.io.FileUtil;
|
||||
import cn.hutool.core.io.IORuntimeException;
|
||||
import cn.iocoder.yudao.module.infra.framework.file.core.client.AbstractFileClient;
|
||||
|
||||
import java.io.File;
|
||||
@ -38,7 +39,14 @@ public class LocalFileClient extends AbstractFileClient<LocalFileClientConfig> {
|
||||
@Override
|
||||
public byte[] getContent(String path) {
|
||||
String filePath = getFilePath(path);
|
||||
return FileUtil.readBytes(filePath);
|
||||
try {
|
||||
return FileUtil.readBytes(filePath);
|
||||
} catch (IORuntimeException ex) {
|
||||
if (ex.getMessage().startsWith("File not exist:")) {
|
||||
return null;
|
||||
}
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
private String getFilePath(String path) {
|
||||
|
||||
@ -1,29 +0,0 @@
|
||||
package cn.iocoder.yudao.module.infra.framework.file.core.client.s3;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 文件预签名地址 Response DTO
|
||||
*
|
||||
* @author owen
|
||||
*/
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class FilePresignedUrlRespDTO {
|
||||
|
||||
/**
|
||||
* 文件上传 URL(用于上传)
|
||||
*
|
||||
* 例如说:
|
||||
*/
|
||||
private String uploadUrl;
|
||||
|
||||
/**
|
||||
* 文件 URL(用于读取、下载等)
|
||||
*/
|
||||
private String url;
|
||||
|
||||
}
|
||||
@ -1,8 +1,10 @@
|
||||
package cn.iocoder.yudao.module.infra.framework.file.core.client.s3;
|
||||
|
||||
import cn.hutool.core.io.IoUtil;
|
||||
import cn.hutool.core.util.BooleanUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.http.HttpUtil;
|
||||
import cn.iocoder.yudao.framework.common.util.http.HttpUtils;
|
||||
import cn.iocoder.yudao.module.infra.framework.file.core.client.AbstractFileClient;
|
||||
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
|
||||
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
|
||||
@ -15,9 +17,11 @@ import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
|
||||
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
|
||||
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
|
||||
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
|
||||
import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest;
|
||||
import software.amazon.awssdk.services.s3.presigner.model.PutObjectPresignRequest;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.time.Duration;
|
||||
|
||||
/**
|
||||
@ -27,6 +31,8 @@ import java.time.Duration;
|
||||
*/
|
||||
public class S3FileClient extends AbstractFileClient<S3FileClientConfig> {
|
||||
|
||||
private static final Duration EXPIRATION_DEFAULT = Duration.ofHours(24);
|
||||
|
||||
private S3Client client;
|
||||
private S3Presigner presigner;
|
||||
|
||||
@ -75,7 +81,7 @@ public class S3FileClient extends AbstractFileClient<S3FileClientConfig> {
|
||||
// 上传文件
|
||||
client.putObject(putRequest, RequestBody.fromBytes(content));
|
||||
// 拼接返回路径
|
||||
return config.getDomain() + "/" + path;
|
||||
return presignGetUrl(path, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -97,23 +103,33 @@ public class S3FileClient extends AbstractFileClient<S3FileClientConfig> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public FilePresignedUrlRespDTO getPresignedObjectUrl(String path) {
|
||||
Duration expiration = Duration.ofHours(24);
|
||||
return new FilePresignedUrlRespDTO(getPresignedUrl(path, expiration), config.getDomain() + "/" + path);
|
||||
public String presignPutUrl(String path) {
|
||||
return presigner.presignPutObject(PutObjectPresignRequest.builder()
|
||||
.signatureDuration(EXPIRATION_DEFAULT)
|
||||
.putObjectRequest(b -> b.bucket(config.getBucket()).key(path)).build())
|
||||
.url().toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成动态的预签名上传 URL
|
||||
*
|
||||
* @param path 相对路径
|
||||
* @param expiration 过期时间
|
||||
* @return 生成的上传 URL
|
||||
*/
|
||||
private String getPresignedUrl(String path, Duration expiration) {
|
||||
return presigner.presignPutObject(PutObjectPresignRequest.builder()
|
||||
@Override
|
||||
public String presignGetUrl(String url, Integer expirationSeconds) {
|
||||
// 1. 将 url 转换为 path
|
||||
String path = StrUtil.removePrefix(url, config.getDomain() + "/");
|
||||
path = HttpUtils.removeUrlQuery(path);
|
||||
|
||||
// 2.1 情况一:公开访问:无需签名
|
||||
// 考虑到老版本的兼容,所以必须是 config.getEnablePublicAccess() 为 false 时,才进行签名
|
||||
if (!BooleanUtil.isFalse(config.getEnablePublicAccess())) {
|
||||
return config.getDomain() + "/" + path;
|
||||
}
|
||||
|
||||
// 2.2 情况二:私有访问:生成 GET 预签名 URL
|
||||
String finalPath = path;
|
||||
Duration expiration = expirationSeconds != null ? Duration.ofSeconds(expirationSeconds) : EXPIRATION_DEFAULT;
|
||||
URL signedUrl = presigner.presignGetObject(GetObjectPresignRequest.builder()
|
||||
.signatureDuration(expiration)
|
||||
.putObjectRequest(b -> b.bucket(config.getBucket()).key(path))
|
||||
.build()).url().toString();
|
||||
.getObjectRequest(b -> b.bucket(config.getBucket()).key(finalPath)).build())
|
||||
.url();
|
||||
return signedUrl.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -73,6 +73,15 @@ public class S3FileClientConfig implements FileClientConfig {
|
||||
@NotNull(message = "enablePathStyleAccess 不能为空")
|
||||
private Boolean enablePathStyleAccess;
|
||||
|
||||
/**
|
||||
* 是否公开访问
|
||||
*
|
||||
* true:公开访问,所有人都可以访问
|
||||
* false:私有访问,只有配置的 accessKey 才可以访问
|
||||
*/
|
||||
@NotNull(message = "是否公开访问不能为空")
|
||||
private Boolean enablePublicAccess;
|
||||
|
||||
@SuppressWarnings("RedundantIfStatement")
|
||||
@AssertTrue(message = "domain 不能为空")
|
||||
@JsonIgnore
|
||||
|
||||
@ -80,11 +80,17 @@ public class FileTypeUtils {
|
||||
*/
|
||||
public static void writeAttachment(HttpServletResponse response, String filename, byte[] content) throws IOException {
|
||||
// 设置 header 和 contentType
|
||||
response.setHeader("Content-Disposition", "attachment;filename=" + HttpUtils.encodeUtf8(filename));
|
||||
String contentType = getMineType(content, filename);
|
||||
response.setContentType(contentType);
|
||||
String mineType = getMineType(content, filename);
|
||||
response.setContentType(mineType);
|
||||
// 设置内容显示、下载文件名:https://www.cnblogs.com/wq-9/articles/12165056.html
|
||||
if (isImage(mineType)) {
|
||||
// 参见 https://github.com/YunaiV/ruoyi-vue-pro/issues/692 讨论
|
||||
response.setHeader("Content-Disposition", "inline;filename=" + HttpUtils.encodeUtf8(filename));
|
||||
} else {
|
||||
response.setHeader("Content-Disposition", "attachment;filename=" + HttpUtils.encodeUtf8(filename));
|
||||
}
|
||||
// 针对 video 的特殊处理,解决视频地址在移动端播放的兼容性问题
|
||||
if (StrUtil.containsIgnoreCase(contentType, "video")) {
|
||||
if (StrUtil.containsIgnoreCase(mineType, "video")) {
|
||||
response.setHeader("Content-Length", String.valueOf(content.length));
|
||||
response.setHeader("Content-Range", "bytes 0-" + (content.length - 1) + "/" + content.length);
|
||||
response.setHeader("Accept-Ranges", "bytes");
|
||||
@ -93,4 +99,14 @@ public class FileTypeUtils {
|
||||
IoUtil.write(response.getOutputStream(), false, content);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否是图片
|
||||
*
|
||||
* @param mineType 类型
|
||||
* @return 是否是图片
|
||||
*/
|
||||
public static boolean isImage(String mineType) {
|
||||
return StrUtil.startWith(mineType, "image/");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -37,14 +37,22 @@ public interface FileService {
|
||||
String name, String directory, String type);
|
||||
|
||||
/**
|
||||
* 生成文件预签名地址信息
|
||||
* 生成文件预签名地址信息,用于上传
|
||||
*
|
||||
* @param name 文件名
|
||||
* @param directory 目录
|
||||
* @return 预签名地址信息
|
||||
*/
|
||||
FilePresignedUrlRespVO getFilePresignedUrl(@NotEmpty(message = "文件名不能为空") String name,
|
||||
String directory);
|
||||
FilePresignedUrlRespVO presignPutUrl(@NotEmpty(message = "文件名不能为空") String name,
|
||||
String directory);
|
||||
/**
|
||||
* 生成文件预签名地址信息,用于读取
|
||||
*
|
||||
* @param url 完整的文件访问地址
|
||||
* @param expirationSeconds 访问有效期,单位秒
|
||||
* @return 文件预签名地址
|
||||
*/
|
||||
String presignGetUrl(String url, Integer expirationSeconds);
|
||||
|
||||
/**
|
||||
* 创建文件
|
||||
|
||||
@ -6,6 +6,7 @@ import cn.hutool.core.lang.Assert;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.crypto.digest.DigestUtil;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.common.util.http.HttpUtils;
|
||||
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
||||
import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FileCreateReqVO;
|
||||
import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePageReqVO;
|
||||
@ -13,7 +14,6 @@ import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePresigned
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO;
|
||||
import cn.iocoder.yudao.module.infra.dal.mysql.file.FileMapper;
|
||||
import cn.iocoder.yudao.module.infra.framework.file.core.client.FileClient;
|
||||
import cn.iocoder.yudao.module.infra.framework.file.core.client.s3.FilePresignedUrlRespDTO;
|
||||
import cn.iocoder.yudao.module.infra.framework.file.core.utils.FileTypeUtils;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import lombok.SneakyThrows;
|
||||
@ -126,19 +126,27 @@ public class FileServiceImpl implements FileService {
|
||||
|
||||
@Override
|
||||
@SneakyThrows
|
||||
public FilePresignedUrlRespVO getFilePresignedUrl(String name, String directory) {
|
||||
public FilePresignedUrlRespVO presignPutUrl(String name, String directory) {
|
||||
// 1. 生成上传的 path,需要保证唯一
|
||||
String path = generateUploadPath(name, directory);
|
||||
|
||||
// 2. 获取文件预签名地址
|
||||
FileClient fileClient = fileConfigService.getMasterFileClient();
|
||||
FilePresignedUrlRespDTO presignedObjectUrl = fileClient.getPresignedObjectUrl(path);
|
||||
return BeanUtils.toBean(presignedObjectUrl, FilePresignedUrlRespVO.class,
|
||||
object -> object.setConfigId(fileClient.getId()).setPath(path));
|
||||
String uploadUrl = fileClient.presignPutUrl(path);
|
||||
String visitUrl = fileClient.presignGetUrl(path, null);
|
||||
return new FilePresignedUrlRespVO().setConfigId(fileClient.getId())
|
||||
.setPath(path).setUploadUrl(uploadUrl).setUrl(visitUrl);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String presignGetUrl(String url, Integer expirationSeconds) {
|
||||
FileClient fileClient = fileConfigService.getMasterFileClient();
|
||||
return fileClient.presignGetUrl(url, expirationSeconds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long createFile(FileCreateReqVO createReqVO) {
|
||||
createReqVO.setUrl(HttpUtils.removeUrlQuery(createReqVO.getUrl())); // 目的:移除私有桶情况下,URL 的签名参数
|
||||
FileDO file = BeanUtils.toBean(createReqVO, FileDO.class);
|
||||
fileMapper.insert(file);
|
||||
return file.getId();
|
||||
|
||||
@ -2,7 +2,6 @@ package ${basePackage}.module.${table.moduleName}.service.${table.businessName};
|
||||
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
|
||||
import ${jakartaPackage}.annotation.Resource;
|
||||
|
||||
|
||||
@ -170,6 +170,7 @@
|
||||
await this.#[[$modal]]#.confirm('是否确认删除?')
|
||||
try {
|
||||
await ${simpleClassName}Api.delete${subSimpleClassName}List(this.checkedIds);
|
||||
this.checkedIds = [];
|
||||
await this.getList();
|
||||
this.#[[$modal]]#.msgSuccess("删除成功");
|
||||
} catch {}
|
||||
|
||||
@ -338,6 +338,7 @@ export default {
|
||||
await this.#[[$modal]]#.confirm('是否确认删除?')
|
||||
try {
|
||||
await ${simpleClassName}Api.delete${simpleClassName}List(this.checkedIds);
|
||||
this.checkedIds = [];
|
||||
await this.getList();
|
||||
this.#[[$modal]]#.msgSuccess("删除成功");
|
||||
} catch {}
|
||||
|
||||
@ -209,6 +209,7 @@ const handleDeleteBatch = async () => {
|
||||
// 删除的二次确认
|
||||
await message.delConfirm()
|
||||
await ${simpleClassName}Api.delete${subSimpleClassName}List(checkedIds.value);
|
||||
checkedIds.value = [];
|
||||
message.success(t('common.delSuccess'))
|
||||
await getList();
|
||||
} catch {}
|
||||
|
||||
@ -366,6 +366,7 @@ const handleDeleteBatch = async () => {
|
||||
// 删除的二次确认
|
||||
await message.delConfirm()
|
||||
await ${simpleClassName}Api.delete${simpleClassName}List(checkedIds.value);
|
||||
checkedIds.value = [];
|
||||
message.success(t('common.delSuccess'))
|
||||
await getList();
|
||||
} catch {}
|
||||
|
||||
@ -168,6 +168,7 @@ async function handleDeleteBatch() {
|
||||
});
|
||||
try {
|
||||
await delete${simpleClassName}List(checkedIds.value);
|
||||
checkedIds.value = [];
|
||||
message.success( $t('ui.actionMessage.deleteSuccess') );
|
||||
await getList();
|
||||
} finally {
|
||||
|
||||
@ -92,6 +92,7 @@ async function handleDeleteBatch() {
|
||||
});
|
||||
try {
|
||||
await delete${subSimpleClassName}List(checkedIds.value);
|
||||
checkedIds.value = [];
|
||||
message.success( $t('ui.actionMessage.deleteSuccess') );
|
||||
await getList();
|
||||
} finally {
|
||||
|
||||
@ -102,6 +102,7 @@ async function handleDeleteBatch() {
|
||||
});
|
||||
try {
|
||||
await delete${simpleClassName}List(checkedIds.value);
|
||||
checkedIds.value = [];
|
||||
message.success({
|
||||
content: $t('ui.actionMessage.deleteSuccess'),
|
||||
key: 'action_key_msg',
|
||||
|
||||
@ -82,6 +82,7 @@ async function handleDeleteBatch() {
|
||||
});
|
||||
try {
|
||||
await delete${subSimpleClassName}List(checkedIds.value);
|
||||
checkedIds.value = [];
|
||||
message.success({
|
||||
content: $t('ui.actionMessage.deleteSuccess', [row.id]),
|
||||
key: 'action_key_msg',
|
||||
|
||||
@ -163,6 +163,7 @@ async function handleDeleteBatch() {
|
||||
});
|
||||
try {
|
||||
await delete${simpleClassName}List(checkedIds.value);
|
||||
checkedIds.value = [];
|
||||
ElMessage.success($t('ui.actionMessage.deleteSuccess'));
|
||||
await getList();
|
||||
} finally {
|
||||
|
||||
@ -87,6 +87,7 @@ async function handleDeleteBatch() {
|
||||
});
|
||||
try {
|
||||
await delete${subSimpleClassName}List(checkedIds.value);
|
||||
checkedIds.value = [];
|
||||
ElMessage.success($t('ui.actionMessage.deleteSuccess'));
|
||||
await getList();
|
||||
} finally {
|
||||
|
||||
@ -99,6 +99,7 @@ async function handleDeleteBatch() {
|
||||
});
|
||||
try {
|
||||
await delete${simpleClassName}List(checkedIds.value);
|
||||
checkedIds.value = [];
|
||||
ElMessage.success($t('ui.actionMessage.deleteSuccess'));
|
||||
onRefresh();
|
||||
} finally {
|
||||
|
||||
@ -79,6 +79,7 @@ async function handleDeleteBatch() {
|
||||
});
|
||||
try {
|
||||
await delete${subSimpleClassName}List(checkedIds.value);
|
||||
checkedIds.value = [];
|
||||
ElMessage.success($t('ui.actionMessage.deleteSuccess'));
|
||||
onRefresh();
|
||||
} finally {
|
||||
|
||||
@ -0,0 +1,61 @@
|
||||
package cn.iocoder.yudao.module.iot.api.device;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.enums.RpcConstants;
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
|
||||
import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
|
||||
import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO;
|
||||
import cn.iocoder.yudao.module.iot.service.device.IotDeviceService;
|
||||
import cn.iocoder.yudao.module.iot.service.product.IotProductService;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
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.RestController;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.annotation.security.PermitAll;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||
|
||||
/**
|
||||
* IoT 设备 API 实现类
|
||||
*
|
||||
* @author haohao
|
||||
*/
|
||||
@RestController
|
||||
@Validated
|
||||
@Primary // 保证优先匹配,因为 yudao-iot-gateway 也有 IotDeviceCommonApi 的实现,并且也可能会被 biz 引入
|
||||
public class IoTDeviceApiImpl implements IotDeviceCommonApi {
|
||||
|
||||
@Resource
|
||||
private IotDeviceService deviceService;
|
||||
@Resource
|
||||
private IotProductService productService;
|
||||
|
||||
@Override
|
||||
@PostMapping(RpcConstants.RPC_API_PREFIX + "/iot/device/auth")
|
||||
@PermitAll
|
||||
public CommonResult<Boolean> authDevice(@RequestBody IotDeviceAuthReqDTO authReqDTO) {
|
||||
return success(deviceService.authDevice(authReqDTO));
|
||||
}
|
||||
|
||||
@Override
|
||||
@PostMapping(RpcConstants.RPC_API_PREFIX + "/iot/device/get") // 特殊:方便调用,暂时使用 POST,实际更推荐 GET
|
||||
@PermitAll
|
||||
public CommonResult<IotDeviceRespDTO> getDevice(@RequestBody IotDeviceGetReqDTO getReqDTO) {
|
||||
IotDeviceDO device = getReqDTO.getId() != null ? deviceService.getDeviceFromCache(getReqDTO.getId())
|
||||
: deviceService.getDeviceFromCache(getReqDTO.getProductKey(), getReqDTO.getDeviceName());
|
||||
return success(BeanUtils.toBean(device, IotDeviceRespDTO.class, deviceDTO -> {
|
||||
IotProductDO product = productService.getProductFromCache(deviceDTO.getProductId());
|
||||
if (product != null) {
|
||||
deviceDTO.setCodecType(product.getCodecType());
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,104 @@
|
||||
package cn.iocoder.yudao.module.iot.controller.admin.alert;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
|
||||
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.module.iot.controller.admin.alert.vo.config.IotAlertConfigPageReqVO;
|
||||
import cn.iocoder.yudao.module.iot.controller.admin.alert.vo.config.IotAlertConfigRespVO;
|
||||
import cn.iocoder.yudao.module.iot.controller.admin.alert.vo.config.IotAlertConfigSaveReqVO;
|
||||
import cn.iocoder.yudao.module.iot.dal.dataobject.alert.IotAlertConfigDO;
|
||||
import cn.iocoder.yudao.module.iot.service.alert.IotAlertConfigService;
|
||||
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
|
||||
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.validation.Valid;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
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.convertSetByFlatMap;
|
||||
|
||||
@Tag(name = "管理后台 - IoT 告警配置")
|
||||
@RestController
|
||||
@RequestMapping("/iot/alert-config")
|
||||
@Validated
|
||||
public class IotAlertConfigController {
|
||||
|
||||
@Resource
|
||||
private IotAlertConfigService alertConfigService;
|
||||
|
||||
@Resource
|
||||
private AdminUserApi adminUserApi;
|
||||
|
||||
@PostMapping("/create")
|
||||
@Operation(summary = "创建告警配置")
|
||||
@PreAuthorize("@ss.hasPermission('iot:alert-config:create')")
|
||||
public CommonResult<Long> createAlertConfig(@Valid @RequestBody IotAlertConfigSaveReqVO createReqVO) {
|
||||
return success(alertConfigService.createAlertConfig(createReqVO));
|
||||
}
|
||||
|
||||
@PutMapping("/update")
|
||||
@Operation(summary = "更新告警配置")
|
||||
@PreAuthorize("@ss.hasPermission('iot:alert-config:update')")
|
||||
public CommonResult<Boolean> updateAlertConfig(@Valid @RequestBody IotAlertConfigSaveReqVO updateReqVO) {
|
||||
alertConfigService.updateAlertConfig(updateReqVO);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@DeleteMapping("/delete")
|
||||
@Operation(summary = "删除告警配置")
|
||||
@Parameter(name = "id", description = "编号", required = true)
|
||||
@PreAuthorize("@ss.hasPermission('iot:alert-config:delete')")
|
||||
public CommonResult<Boolean> deleteAlertConfig(@RequestParam("id") Long id) {
|
||||
alertConfigService.deleteAlertConfig(id);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@GetMapping("/get")
|
||||
@Operation(summary = "获得告警配置")
|
||||
@Parameter(name = "id", description = "编号", required = true, example = "1024")
|
||||
@PreAuthorize("@ss.hasPermission('iot:alert-config:query')")
|
||||
public CommonResult<IotAlertConfigRespVO> getAlertConfig(@RequestParam("id") Long id) {
|
||||
IotAlertConfigDO alertConfig = alertConfigService.getAlertConfig(id);
|
||||
return success(BeanUtils.toBean(alertConfig, IotAlertConfigRespVO.class));
|
||||
}
|
||||
|
||||
@GetMapping("/page")
|
||||
@Operation(summary = "获得告警配置分页")
|
||||
@PreAuthorize("@ss.hasPermission('iot:alert-config:query')")
|
||||
public CommonResult<PageResult<IotAlertConfigRespVO>> getAlertConfigPage(@Valid IotAlertConfigPageReqVO pageReqVO) {
|
||||
PageResult<IotAlertConfigDO> pageResult = alertConfigService.getAlertConfigPage(pageReqVO);
|
||||
|
||||
// 转换返回
|
||||
Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
|
||||
convertSetByFlatMap(pageResult.getList(), config -> config.getReceiveUserIds().stream()));
|
||||
return success(BeanUtils.toBean(pageResult, IotAlertConfigRespVO.class, vo -> {
|
||||
vo.setReceiveUserNames(vo.getReceiveUserIds().stream()
|
||||
.map(userMap::get)
|
||||
.filter(Objects::nonNull)
|
||||
.map(AdminUserRespDTO::getNickname)
|
||||
.collect(Collectors.toList()));
|
||||
}));
|
||||
}
|
||||
|
||||
@GetMapping("/simple-list")
|
||||
@Operation(summary = "获得告警配置简单列表", description = "只包含被开启的告警配置,主要用于前端的下拉选项")
|
||||
@PreAuthorize("@ss.hasPermission('iot:alert-config:query')")
|
||||
public CommonResult<List<IotAlertConfigRespVO>> getAlertConfigSimpleList() {
|
||||
List<IotAlertConfigDO> list = alertConfigService.getAlertConfigListByStatus(CommonStatusEnum.ENABLE.getStatus());
|
||||
return success(convertList(list, config -> // 只返回 id、name 字段
|
||||
new IotAlertConfigRespVO().setId(config.getId()).setName(config.getName())));
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,58 @@
|
||||
package cn.iocoder.yudao.module.iot.controller.admin.alert;
|
||||
|
||||
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.module.iot.controller.admin.alert.vo.recrod.IotAlertRecordPageReqVO;
|
||||
import cn.iocoder.yudao.module.iot.controller.admin.alert.vo.recrod.IotAlertRecordProcessReqVO;
|
||||
import cn.iocoder.yudao.module.iot.controller.admin.alert.vo.recrod.IotAlertRecordRespVO;
|
||||
import cn.iocoder.yudao.module.iot.dal.dataobject.alert.IotAlertRecordDO;
|
||||
import cn.iocoder.yudao.module.iot.service.alert.IotAlertRecordService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.validation.Valid;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||
import static java.util.Collections.singleton;
|
||||
|
||||
@Tag(name = "管理后台 - IoT 告警记录")
|
||||
@RestController
|
||||
@RequestMapping("/iot/alert-record")
|
||||
@Validated
|
||||
public class IotAlertRecordController {
|
||||
|
||||
@Resource
|
||||
private IotAlertRecordService alertRecordService;
|
||||
|
||||
@GetMapping("/get")
|
||||
@Operation(summary = "获得告警记录")
|
||||
@Parameter(name = "id", description = "编号", required = true, example = "1024")
|
||||
@PreAuthorize("@ss.hasPermission('iot:alert-record:query')")
|
||||
public CommonResult<IotAlertRecordRespVO> getAlertRecord(@RequestParam("id") Long id) {
|
||||
IotAlertRecordDO alertRecord = alertRecordService.getAlertRecord(id);
|
||||
return success(BeanUtils.toBean(alertRecord, IotAlertRecordRespVO.class));
|
||||
}
|
||||
|
||||
@GetMapping("/page")
|
||||
@Operation(summary = "获得告警记录分页")
|
||||
@PreAuthorize("@ss.hasPermission('iot:alert-record:query')")
|
||||
public CommonResult<PageResult<IotAlertRecordRespVO>> getAlertRecordPage(@Valid IotAlertRecordPageReqVO pageReqVO) {
|
||||
PageResult<IotAlertRecordDO> pageResult = alertRecordService.getAlertRecordPage(pageReqVO);
|
||||
return success(BeanUtils.toBean(pageResult, IotAlertRecordRespVO.class));
|
||||
}
|
||||
|
||||
@PutMapping("/process")
|
||||
@Operation(summary = "处理告警记录")
|
||||
@PreAuthorize("@ss.hasPermission('iot:alert-record:process')")
|
||||
public CommonResult<Boolean> processAlertRecord(@Valid @RequestBody IotAlertRecordProcessReqVO processReqVO) {
|
||||
alertRecordService.processAlertRecordList(singleton(processReqVO.getId()), processReqVO.getProcessRemark());
|
||||
return success(true);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
package cn.iocoder.yudao.module.iot.controller.admin.alert.vo.config;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageParam;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
|
||||
|
||||
@Schema(description = "管理后台 - IoT 告警配置分页 Request VO")
|
||||
@Data
|
||||
public class IotAlertConfigPageReqVO extends PageParam {
|
||||
|
||||
@Schema(description = "配置名称", example = "赵六")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "配置状态", example = "1")
|
||||
private Integer status;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
|
||||
private LocalDateTime[] createTime;
|
||||
|
||||
}
|
||||
@ -0,0 +1,43 @@
|
||||
package cn.iocoder.yudao.module.iot.controller.admin.alert.vo.config;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Schema(description = "管理后台 - IoT 告警配置 Response VO")
|
||||
@Data
|
||||
public class IotAlertConfigRespVO {
|
||||
|
||||
@Schema(description = "配置编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3566")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "配置名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "赵六")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "配置描述", example = "你猜")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "告警级别", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||
private Integer level;
|
||||
|
||||
@Schema(description = "配置状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||
private Integer status;
|
||||
|
||||
@Schema(description = "关联的场景联动规则编号数组", requiredMode = Schema.RequiredMode.REQUIRED, example = "1,2,3")
|
||||
private List<Long> sceneRuleIds;
|
||||
|
||||
@Schema(description = "接收的用户编号数组", requiredMode = Schema.RequiredMode.REQUIRED, example = "100,200")
|
||||
private List<Long> receiveUserIds;
|
||||
|
||||
@Schema(description = "接收的用户名称数组", requiredMode = Schema.RequiredMode.REQUIRED, example = "张三,李四")
|
||||
private List<String> receiveUserNames;
|
||||
|
||||
@Schema(description = "接收的类型数组", requiredMode = Schema.RequiredMode.REQUIRED, example = "1,2,3")
|
||||
private List<Integer> receiveTypes;
|
||||
|
||||
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private LocalDateTime createTime;
|
||||
|
||||
}
|
||||
@ -0,0 +1,47 @@
|
||||
package cn.iocoder.yudao.module.iot.controller.admin.alert.vo.config;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
|
||||
import cn.iocoder.yudao.framework.common.validation.InEnum;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.List;
|
||||
|
||||
@Schema(description = "管理后台 - IoT 告警配置新增/修改 Request VO")
|
||||
@Data
|
||||
public class IotAlertConfigSaveReqVO {
|
||||
|
||||
@Schema(description = "配置编号", example = "3566")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "配置名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "赵六")
|
||||
@NotEmpty(message = "配置名称不能为空")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "配置描述", example = "你猜")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "告警级别", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||
@NotNull(message = "告警级别不能为空")
|
||||
private Integer level;
|
||||
|
||||
@Schema(description = "配置状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||
@NotNull(message = "配置状态不能为空")
|
||||
@InEnum(CommonStatusEnum.class)
|
||||
private Integer status;
|
||||
|
||||
@Schema(description = "关联的场景联动规则编号数组")
|
||||
@NotEmpty(message = "关联的场景联动规则编号数组不能为空")
|
||||
private List<Long> sceneRuleIds;
|
||||
|
||||
@Schema(description = "接收的用户编号数组")
|
||||
@NotEmpty(message = "接收的用户编号数组不能为空")
|
||||
private List<Long> receiveUserIds;
|
||||
|
||||
@Schema(description = "接收的类型数组")
|
||||
@NotEmpty(message = "接收的类型数组不能为空")
|
||||
private List<Integer> receiveTypes;
|
||||
|
||||
}
|
||||
@ -0,0 +1,35 @@
|
||||
package cn.iocoder.yudao.module.iot.controller.admin.alert.vo.recrod;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageParam;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
|
||||
|
||||
@Schema(description = "管理后台 - IoT 告警记录分页 Request VO")
|
||||
@Data
|
||||
public class IotAlertRecordPageReqVO extends PageParam {
|
||||
|
||||
@Schema(description = "告警配置编号", example = "29320")
|
||||
private Long configId;
|
||||
|
||||
@Schema(description = "告警级别", example = "1")
|
||||
private Integer level;
|
||||
|
||||
@Schema(description = "产品编号", example = "2050")
|
||||
private Long productId;
|
||||
|
||||
@Schema(description = "设备编号", example = "21727")
|
||||
private String deviceId;
|
||||
|
||||
@Schema(description = "是否处理", example = "true")
|
||||
private Boolean processStatus;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
|
||||
private LocalDateTime[] createTime;
|
||||
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
package cn.iocoder.yudao.module.iot.controller.admin.alert.vo.recrod;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
@Schema(description = "管理后台 - IoT 告警记录处理 Request VO")
|
||||
@Data
|
||||
public class IotAlertRecordProcessReqVO {
|
||||
|
||||
@Schema(description = "记录编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
@NotNull(message = "记录编号不能为空")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "处理结果(备注)", requiredMode = Schema.RequiredMode.REQUIRED, example = "已处理告警,问题已解决")
|
||||
private String processRemark;
|
||||
|
||||
}
|
||||
@ -0,0 +1,43 @@
|
||||
package cn.iocoder.yudao.module.iot.controller.admin.alert.vo.recrod;
|
||||
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Schema(description = "管理后台 - IoT 告警记录 Response VO")
|
||||
@Data
|
||||
public class IotAlertRecordRespVO {
|
||||
|
||||
@Schema(description = "记录编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "19904")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "告警配置编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "29320")
|
||||
private Long configId;
|
||||
|
||||
@Schema(description = "告警名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "张三")
|
||||
private String configName;
|
||||
|
||||
@Schema(description = "告警级别", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||
private Integer configLevel;
|
||||
|
||||
@Schema(description = "产品编号", example = "2050")
|
||||
private Long productId;
|
||||
|
||||
@Schema(description = "设备编号", example = "21727")
|
||||
private Long deviceId;
|
||||
|
||||
@Schema(description = "触发的设备消息")
|
||||
private IotDeviceMessage deviceMessage;
|
||||
|
||||
@Schema(description = "是否处理", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||
private Boolean processStatus;
|
||||
|
||||
@Schema(description = "处理结果(备注)", example = "你说的对")
|
||||
private String processRemark;
|
||||
|
||||
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private LocalDateTime createTime;
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user