# 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:
YunaiV
2025-08-31 10:40:14 +08:00
327 changed files with 20840 additions and 452 deletions

View File

@@ -21,13 +21,15 @@ public class MailSendApiImpl implements MailSendApi {
@Override
public Long sendSingleMailToAdmin(MailSendSingleToUserReqDTO reqDTO) {
return mailSendService.sendSingleMailToAdmin(reqDTO.getMail(), reqDTO.getUserId(),
return mailSendService.sendSingleMailToAdmin(reqDTO.getUserId(),
reqDTO.getToMails(), reqDTO.getCcMails(), reqDTO.getBccMails(),
reqDTO.getTemplateCode(), reqDTO.getTemplateParams());
}
@Override
public Long sendSingleMailToMember(MailSendSingleToUserReqDTO reqDTO) {
return mailSendService.sendSingleMailToMember(reqDTO.getMail(), reqDTO.getUserId(),
return mailSendService.sendSingleMailToMember(reqDTO.getUserId(),
reqDTO.getToMails(), reqDTO.getCcMails(), reqDTO.getBccMails(),
reqDTO.getTemplateCode(), reqDTO.getTemplateParams());
}

View File

@@ -4,6 +4,7 @@ import lombok.Data;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotNull;
import java.util.List;
import java.util.Map;
/**
@@ -16,13 +17,24 @@ public class MailSendSingleToUserReqDTO {
/**
* 用户编号
*
* 如果非空,则加载对应用户的邮箱,添加到 {@link #toMails} 中
*/
private Long userId;
/**
* 邮箱
* 收件邮箱
*/
@Email
private String mail;
private List<@Email String> toMails;
/**
* 抄送邮箱
*/
private List<@Email String> ccMails;
/**
* 密送邮箱
*/
private List<@Email String> bccMails;
/**
* 邮件模板编号

View File

@@ -11,6 +11,24 @@ tag: Yunai.local
"code": "1024"
}
### 请求 /login 接口【加密 AES】 => 成功
POST {{baseUrl}}/system/auth/login
Content-Type: application/json
tenant-id: {{adminTenantId}}
tag: Yunai.local
X-API-ENCRYPT: true
WvSX9MOrenyGfBhEM0g1/hHgq8ocktMZ9OwAJ6MOG5FUrzYF/rG5JF1eMptQM1wT73VgDS05l/37WeRtad+JrqChAul/sR/SdOsUKqjBhvvQx1JVhzxr6s8uUP67aKTSZ6Psv7O32ELxXrzSaQvG5CInzz3w6sLtbNNLd1kXe6Q=
### 请求 /login 接口【加密 RSA】 => 成功
POST {{baseUrl}}/system/auth/login
Content-Type: application/json
tenant-id: {{adminTenantId}}
tag: Yunai.local
X-API-ENCRYPT: true
e7QZTork9ZV5CmgZvSd+cHZk3xdUxKtowLM02kOha+gxHK2H/daU8nVBYS3+bwuDRy5abf+Pz1QJJGVAEd27wwrXBmupOOA/bhpuzzDwcRuJRD+z+YgiNoEXFDRHERxPYlPqAe9zAHtihD0ceub1AjybQsEsROew4C3Q602XYW0=
### 请求 /login 接口 => 成功(无验证码)
POST {{baseUrl}}/system/auth/login
Content-Type: application/json

View File

@@ -56,6 +56,15 @@ public class DeptController {
return success(true);
}
@DeleteMapping("/delete-list")
@Operation(summary = "批量删除部门")
@Parameter(name = "ids", description = "编号列表", required = true)
@PreAuthorize("@ss.hasPermission('system:dept:delete')")
public CommonResult<Boolean> deleteDeptList(@RequestParam("ids") List<Long> ids) {
deptService.deleteDeptList(ids);
return success(true);
}
@GetMapping("/list")
@Operation(summary = "获取部门列表")
@PreAuthorize("@ss.hasPermission('system:dept:query')")

View File

@@ -91,7 +91,8 @@ public class MailTemplateController {
@Operation(summary = "发送短信")
@PreAuthorize("@ss.hasPermission('system:mail-template:send-mail')")
public CommonResult<Long> sendMail(@Valid @RequestBody MailTemplateSendReqVO sendReqVO) {
return success(mailSendService.sendSingleMailToAdmin(sendReqVO.getMail(), getLoginUserId(),
return success(mailSendService.sendSingleMailToAdmin(getLoginUserId(),
sendReqVO.getToMails(), sendReqVO.getCcMails(), sendReqVO.getBccMails(),
sendReqVO.getTemplateCode(), sendReqVO.getTemplateParams()));
}

View File

@@ -2,13 +2,11 @@ package cn.iocoder.yudao.module.system.controller.admin.mail.vo.log;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@Schema(description = "管理后台 - 邮件日志 Response VO")
@Data
public class MailLogRespVO {
@@ -22,8 +20,14 @@ public class MailLogRespVO {
@Schema(description = "用户类型,参见 UserTypeEnum 枚举", example = "2")
private Byte userType;
@Schema(description = "接收邮箱地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "76854@qq.com")
private String toMail;
@Schema(description = "接收邮箱地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "user1@example.com, user2@example.com")
private List<String> toMails;
@Schema(description = "抄送邮箱地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "user3@example.com, user4@example.com")
private List<String> ccMails;
@Schema(description = "密送邮箱地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "user5@example.com, user6@example.com")
private List<String> bccMails;
@Schema(description = "邮箱账号编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "18107")
private Long accountId;

View File

@@ -5,15 +5,22 @@ import lombok.Data;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.util.List;
import java.util.Map;
@Schema(description = "管理后台 - 邮件发送 Req VO")
@Data
public class MailTemplateSendReqVO {
@Schema(description = "接收邮箱", requiredMode = Schema.RequiredMode.REQUIRED, example = "7685413@qq.com")
@Schema(description = "接收邮箱", requiredMode = Schema.RequiredMode.REQUIRED, example = "[user1@example.com, user2@example.com]")
@NotEmpty(message = "接收邮箱不能为空")
private String mail;
private List<String> toMails;
@Schema(description = "抄送邮箱", requiredMode = Schema.RequiredMode.REQUIRED, example = "[user3@example.com, user4@example.com]")
private List<String> ccMails;
@Schema(description = "密送邮箱", requiredMode = Schema.RequiredMode.REQUIRED, example = "[user5@example.com, user6@example.com]")
private List<String> bccMails;
@Schema(description = "模板编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "test_01")
@NotNull(message = "模板编码不能为空")

View File

@@ -35,6 +35,14 @@ tenant-id: {{adminTenantId}}
grant_type=password&username=admin&password=admin123&scope=user.read
### 请求 /system/oauth2/token + client_credentials 接口 => 成功
POST {{baseUrl}}/system/oauth2/token
Content-Type: application/x-www-form-urlencoded
Authorization: Basic ZGVmYXVsdDphZG1pbjEyMw==
tenant-id: {{adminTenantId}}
grant_type=client_credentials&scope=user.read
### 请求 /system/oauth2/token + refresh_token 接口 => 成功
POST {{baseUrl}}/system/oauth2/token
Content-Type: application/x-www-form-urlencoded

View File

@@ -94,6 +94,7 @@ public class OAuth2OpenController {
@Parameter(name = "scope", example = "user_info"),
@Parameter(name = "refresh_token", example = "123424233"),
})
@SuppressWarnings("EnhancedSwitchMigration")
public CommonResult<OAuth2OpenAccessTokenRespVO> postAccessToken(HttpServletRequest request,
@RequestParam("grant_type") String grantType,
@RequestParam(value = "code", required = false) String code, // 授权码模式

View File

@@ -1,14 +1,15 @@
package cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant;
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
import cn.idev.excel.annotation.ExcelProperty;
import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
import cn.iocoder.yudao.module.system.enums.DictTypeConstants;
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
import cn.idev.excel.annotation.ExcelProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
@Schema(description = "管理后台 - 租户 Response VO")
@Data
@@ -36,8 +37,8 @@ public class TenantRespVO {
@DictFormat(DictTypeConstants.COMMON_STATUS)
private Integer status;
@Schema(description = "绑定域名", example = "https://www.iocoder.cn")
private String website;
@Schema(description = "绑定域名数组", example = "https://www.iocoder.cn")
private List<String> websites;
@Schema(description = "租户套餐编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long packageId;

View File

@@ -11,6 +11,7 @@ import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
import java.time.LocalDateTime;
import java.util.List;
@Schema(description = "管理后台 - 租户创建/修改 Request VO")
@Data
@@ -34,8 +35,8 @@ public class TenantSaveReqVO {
@NotNull(message = "租户状态")
private Integer status;
@Schema(description = "绑定域名", example = "https://www.iocoder.cn")
private String website;
@Schema(description = "绑定域名数组", example = "https://www.iocoder.cn")
private List<String> websites;
@Schema(description = "租户套餐编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "租户套餐编号不能为空")

View File

@@ -1,14 +1,13 @@
package cn.iocoder.yudao.module.system.controller.admin.user.vo.user;
import cn.idev.excel.annotation.ExcelProperty;
import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
import cn.iocoder.yudao.module.system.enums.DictTypeConstants;
import cn.idev.excel.annotation.ExcelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
/**
* 用户 Excel 导入 VO
@@ -17,7 +16,6 @@ import lombok.experimental.Accessors;
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = false) // 设置 chain = false避免用户导入有问题
public class UserImportExcelVO {
@ExcelProperty("登录名称")

View File

@@ -0,0 +1,44 @@
package cn.iocoder.yudao.module.system.controller.app.tenant;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
import cn.iocoder.yudao.module.system.controller.app.tenant.vo.AppTenantRespVO;
import cn.iocoder.yudao.module.system.dal.dataobject.tenant.TenantDO;
import cn.iocoder.yudao.module.system.service.tenant.TenantService;
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.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.annotation.security.PermitAll;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@Tag(name = "用户 App - 租户")
@RestController
@RequestMapping("/system/tenant")
public class AppTenantController {
@Resource
private TenantService tenantService;
@GetMapping("/get-by-website")
@PermitAll
@TenantIgnore
@Operation(summary = "使用域名,获得租户信息", description = "根据用户的域名,获得租户信息")
@Parameter(name = "website", description = "域名", required = true, example = "www.iocoder.cn")
public CommonResult<AppTenantRespVO> getTenantByWebsite(@RequestParam("website") String website) {
TenantDO tenant = tenantService.getTenantByWebsite(website);
if (tenant == null || CommonStatusEnum.isDisable(tenant.getStatus())) {
return success(null);
}
return success(BeanUtils.toBean(tenant, AppTenantRespVO.class));
}
}

View File

@@ -0,0 +1,16 @@
package cn.iocoder.yudao.module.system.controller.app.tenant.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Schema(description = "用户 App - 租户 Response VO")
@Data
public class AppTenantRespVO {
@Schema(description = "租户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long id;
@Schema(description = "租户名", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道")
private String name;
}

View File

@@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.system.dal.dataobject.mail;
import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.framework.mybatis.core.type.StringListTypeHandler;
import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
import cn.iocoder.yudao.module.system.enums.mail.MailSendStatusEnum;
import com.baomidou.mybatisplus.annotation.KeySequence;
@@ -12,6 +13,7 @@ import lombok.*;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
/**
@@ -47,10 +49,22 @@ public class MailLogDO extends BaseDO implements Serializable {
* 枚举 {@link UserTypeEnum}
*/
private Integer userType;
/**
* 接收邮箱地址
*/
private String toMail;
@TableField(typeHandler = StringListTypeHandler.class)
private List<String> toMails;
/**
* 接收邮箱地址
*/
@TableField(typeHandler = StringListTypeHandler.class)
private List<String> ccMails;
/**
* 密送邮箱地址
*/
@TableField(typeHandler = StringListTypeHandler.class)
private List<String> bccMails;
/**
* 邮箱账号编号

View File

@@ -7,8 +7,6 @@ import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.time.LocalDateTime;
import java.util.List;
@@ -22,8 +20,6 @@ import java.util.List;
// 由于 Oracle 的 SEQ 的名字长度有限制,所以就先用 system_oauth2_access_token_seq 吧,反正也没啥问题
@KeySequence("system_oauth2_access_token_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
@Data
@EqualsAndHashCode(callSuper = true)
@Accessors(chain = true)
public class OAuth2RefreshTokenDO extends TenantBaseDO {
/**

View File

@@ -2,13 +2,16 @@ package cn.iocoder.yudao.module.system.dal.dataobject.tenant;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.framework.mybatis.core.type.StringListTypeHandler;
import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.time.LocalDateTime;
import java.util.List;
/**
* 租户 DO
@@ -60,9 +63,13 @@ public class TenantDO extends BaseDO {
*/
private Integer status;
/**
* 绑定域名
* 绑定域名列表
*
* 1. 考虑到对微信小程序的兼容,也允许传递 appid
* 2. 为什么是数组,考虑到管理后台、会员前台都有独立的域名,又或者多个管理后台
*/
private String website;
@TableField(typeHandler = StringListTypeHandler.class)
private List<String> websites;
/**
* 租户套餐编号
*

View File

@@ -1,8 +1,10 @@
package cn.iocoder.yudao.module.system.dal.mysql.mail;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
import cn.iocoder.yudao.module.system.controller.admin.mail.vo.log.MailLogPageReqVO;
import cn.iocoder.yudao.module.system.dal.dataobject.mail.MailLogDO;
import org.apache.ibatis.annotations.Mapper;
@@ -14,11 +16,12 @@ public interface MailLogMapper extends BaseMapperX<MailLogDO> {
return selectPage(reqVO, new LambdaQueryWrapperX<MailLogDO>()
.eqIfPresent(MailLogDO::getUserId, reqVO.getUserId())
.eqIfPresent(MailLogDO::getUserType, reqVO.getUserType())
.likeIfPresent(MailLogDO::getToMail, reqVO.getToMail())
.eqIfPresent(MailLogDO::getAccountId, reqVO.getAccountId())
.eqIfPresent(MailLogDO::getTemplateId, reqVO.getTemplateId())
.eqIfPresent(MailLogDO::getSendStatus, reqVO.getSendStatus())
.betweenIfPresent(MailLogDO::getSendTime, reqVO.getSendTime())
.apply(StrUtil.isNotBlank(reqVO.getToMail()),
MyBatisUtils.findInSet("to_mails", reqVO.getToMail()))
.orderByDesc(MailLogDO::getId));
}

View File

@@ -22,4 +22,8 @@ public interface SmsLogMapper extends BaseMapperX<SmsLogDO> {
.orderByDesc(SmsLogDO::getId));
}
default SmsLogDO selectByApiSerialNo(String apiSerialNo) {
return selectOne(SmsLogDO::getApiSerialNo, apiSerialNo);
}
}

View File

@@ -3,17 +3,13 @@ package cn.iocoder.yudao.module.system.dal.mysql.tenant;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant.TenantPageReqVO;
import cn.iocoder.yudao.module.system.dal.dataobject.tenant.TenantDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
* 租户 Mapper
*
* @author 芋道源码
*/
@Mapper
public interface TenantMapper extends BaseMapperX<TenantDO> {
@@ -31,8 +27,9 @@ public interface TenantMapper extends BaseMapperX<TenantDO> {
return selectOne(TenantDO::getName, name);
}
default TenantDO selectByWebsite(String website) {
return selectOne(TenantDO::getWebsite, website);
default List<TenantDO> selectListByWebsite(String website) {
return selectList(new LambdaQueryWrapperX<TenantDO>()
.apply(MyBatisUtils.findInSet("websites", website)));
}
default Long selectCountByPackageId(Long packageId) {

View File

@@ -9,11 +9,6 @@ import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
* 租户套餐 Mapper
*
* @author 芋道源码
*/
@Mapper
public interface TenantPackageMapper extends BaseMapperX<TenantPackageDO> {

View File

@@ -0,0 +1,212 @@
package cn.iocoder.yudao.module.system.framework.captcha.core;
import com.anji.captcha.model.common.RepCodeEnum;
import com.anji.captcha.model.common.ResponseModel;
import com.anji.captcha.model.vo.CaptchaVO;
import com.anji.captcha.service.impl.AbstractCaptchaService;
import com.anji.captcha.service.impl.CaptchaServiceFactory;
import com.anji.captcha.util.AESUtil;
import com.anji.captcha.util.ImageUtils;
import com.anji.captcha.util.RandomUtils;
import cn.hutool.core.util.RandomUtil;
import org.apache.commons.lang3.Strings;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.util.Properties;
/**
* 图片文字验证码
*
* @author Tsui
* @since 2025/7/23 20:44
*/
public class PictureWordCaptchaServiceImpl extends AbstractCaptchaService {
/**
* 验证码的基础字符
*/
private static final String CHARACTERS = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
/**
* 验证码长度
*/
private static final Integer LENGTH = 4;
private static final int WIDTH = 120;
private static final int HEIGHT = 40;
private static final int LINES = 10;
@Override
public void init(Properties config) {
super.init(config);
}
@Override
public void destroy(Properties config) {
logger.info("start-clear-history-data-{}", captchaType());
}
@Override
public String captchaType() {
return "pictureWord";
}
@Override
public ResponseModel get(CaptchaVO captchaVO) {
String text = generateRandomText(LENGTH);
CaptchaVO imageData = getImageData(text);
// pointJson 不传到前端,只做后端校验,测试时放开
// imageData.setPointJson(text);
return ResponseModel.successData(imageData);
}
@Override
public ResponseModel check(CaptchaVO captchaVO) {
ResponseModel r = super.check(captchaVO);
if (!validatedReq(r)) {
return r;
}
// 取出验证码
String codeKey = String.format(REDIS_CAPTCHA_KEY, captchaVO.getToken());
if (!CaptchaServiceFactory.getCache(cacheType).exists(codeKey)) {
return ResponseModel.errorMsg(RepCodeEnum.API_CAPTCHA_INVALID);
}
// 正确的验证码
String codeValue = CaptchaServiceFactory.getCache(cacheType).get(codeKey);
String code = getCodeByCodeValue(codeValue);
String secretKey = getSecretKeyByCodeValue(codeValue);
// 验证码只用一次,即刻失效
CaptchaServiceFactory.getCache(cacheType).delete(codeKey);
// 用户输入的验证码(CaptchaVO 中 没有预留字段,暂时用 pointJson 无需加解密)
String userCode = captchaVO.getPointJson();
if (!Strings.CI.equals(code, userCode)) {
afterValidateFail(captchaVO);
return ResponseModel.errorMsg(RepCodeEnum.API_CAPTCHA_COORDINATE_ERROR);
}
// 校验成功,将信息存入缓存
String value;
try {
value = AESUtil.aesEncrypt(captchaVO.getToken().concat("---").concat(userCode), secretKey);
} catch (Exception e) {
logger.error("AES 加密失败", e);
afterValidateFail(captchaVO);
return ResponseModel.errorMsg(e.getMessage());
}
String secondKey = String.format(REDIS_SECOND_CAPTCHA_KEY, value);
CaptchaServiceFactory.getCache(cacheType).set(secondKey, captchaVO.getToken(), EXPIRESIN_THREE);
captchaVO.setResult(true);
captchaVO.resetClientFlag();
return ResponseModel.successData(captchaVO);
}
@Override
public ResponseModel verification(CaptchaVO captchaVO) {
ResponseModel r = super.verification(captchaVO);
if (!validatedReq(r)) {
return r;
}
try {
String codeKey = String.format(REDIS_SECOND_CAPTCHA_KEY, captchaVO.getCaptchaVerification());
if (!CaptchaServiceFactory.getCache(cacheType).exists(codeKey)) {
return ResponseModel.errorMsg(RepCodeEnum.API_CAPTCHA_INVALID);
}
// 二次校验取值后,即刻失效
CaptchaServiceFactory.getCache(cacheType).delete(codeKey);
} catch (Exception e) {
logger.error("验证码解析失败", e);
return ResponseModel.errorMsg(e.getMessage());
}
return ResponseModel.success();
}
private CaptchaVO getImageData(String text) {
CaptchaVO dataVO = new CaptchaVO();
BufferedImage image = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_RGB);
Graphics2D g = image.createGraphics();
// 设置背景色
g.setColor(getRandomColor(200, 250));
g.fillRect(0, 0, WIDTH, HEIGHT);
// 绘制干扰线
for (int i = 0; i < LINES; i++) {
g.setColor(getRandomColor(100, 200));
int x1 = RandomUtil.randomInt(WIDTH);
int y1 = RandomUtil.randomInt(HEIGHT);
int x2 = RandomUtil.randomInt(WIDTH);
int y2 = RandomUtil.randomInt(HEIGHT);
g.drawLine(x1, y1, x2, y2);
}
// 设置字体
g.setFont(new Font("Arial", Font.BOLD, 24));
// 绘制验证码文本
for (int i = 0; i < text.length(); i++) {
g.setColor(getRandomColor(20, 130));
// 文字旋转
AffineTransform affineTransform = new AffineTransform();
int x = 20 + i * 20;
int y = 24 + RandomUtil.randomInt(8);
// 旋转范围 -45 ~ 45
affineTransform.setToRotation(Math.toRadians(RandomUtil.randomInt(-45, 45)), x, y);
g.setTransform(affineTransform);
g.drawString(text.charAt(i) + "", x, y);
}
// 添加噪点
for (int i = 0; i < 100; i++) {
int x = RandomUtil.randomInt(WIDTH);
int y = RandomUtil.randomInt(HEIGHT);
image.setRGB(x, y, getRandomColor(0, 255).getRGB());
}
g.dispose();
String secretKey = null;
if (captchaAesStatus) {
secretKey = AESUtil.getKey();
}
dataVO.setSecretKey(secretKey);
dataVO.setOriginalImageBase64(ImageUtils.getImageToBase64Str(image).replaceAll("\r|\n", ""));
dataVO.setToken(RandomUtils.getUUID());
// dataVO.setSecretKey(secretKey);
// 将坐标信息存入 redis 中
String codeKey = String.format(REDIS_CAPTCHA_KEY, dataVO.getToken());
CaptchaServiceFactory.getCache(cacheType).set(codeKey, getCodeValue(text, secretKey), EXPIRESIN_SECONDS);
return dataVO;
}
private String getCodeValue(String text, String secretKey) {
return text + "," + secretKey;
}
private String getCodeByCodeValue(String codeValue) {
return codeValue.split(",")[0];
}
private String getSecretKeyByCodeValue(String codeValue) {
return codeValue.split(",")[1];
}
private Color getRandomColor(int min, int max) {
int minVal = Math.min(min, max);
int maxVal = Math.max(min, max);
int r = RandomUtil.randomInt(minVal, maxVal);
int g = RandomUtil.randomInt(minVal, maxVal);
int b = RandomUtil.randomInt(minVal, maxVal);
return new Color(r, g, b);
}
/**
* 生成指定长度的随机字符串
*
* @param length 长度
* @return {@link String}
*/
public static String generateRandomText(int length) {
return RandomUtil.randomString(CHARACTERS, length);
}
}

View File

@@ -78,7 +78,6 @@ public class AuthRequestFactory {
.keySet()
.stream()
.filter(x -> names.contains(x.toUpperCase()))
.map(String::toUpperCase)
.collect(Collectors.toList());
}
@@ -318,4 +317,4 @@ public class AuthRequestFactory {
.proxy(new Proxy(Proxy.Type.valueOf(proxyConfig.getType()), new InetSocketAddress(proxyConfig.getHostname(), proxyConfig.getPort())))
.build());
}
}
}

View File

@@ -22,8 +22,6 @@ import com.google.common.annotations.VisibleForTesting;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.stream.Collectors;
@@ -113,6 +111,7 @@ public class AliyunSmsClient extends AbstractSmsClient {
}
@VisibleForTesting
@SuppressWarnings("EnhancedSwitchMigration")
Integer convertSmsTemplateAuditStatus(Integer templateStatus) {
switch (templateStatus) {
case 0: return SmsTemplateAuditStatusEnum.CHECKING.getStatus();
@@ -136,20 +135,25 @@ public class AliyunSmsClient extends AbstractSmsClient {
.map(entry -> percentCode(entry.getKey()) + "=" + percentCode(String.valueOf(entry.getValue())))
.collect(Collectors.joining("&"));
// 2.1 请求 Header
// 2. 请求 Body
String requestBody = ""; // 短信 API 为 RPC 接口query parameters 在 uri 中拼接,因此 request body 如果没有特殊要求,设置为空
String hashedRequestPayload = DigestUtil.sha256Hex(requestBody);
// 3.1 请求 Header
TreeMap<String, String> headers = new TreeMap<>();
headers.put("host", HOST);
headers.put("x-acs-version", VERSION);
headers.put("x-acs-action", apiName);
headers.put("x-acs-date", FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mm:ss'Z'", TimeZone.getTimeZone("GMT")).format(new Date()));
headers.put("x-acs-signature-nonce", IdUtil.randomUUID());
headers.put("x-acs-content-sha256", hashedRequestPayload);
// 2.2 构建签名 Header
// 3.2 构建签名 Header
StringBuilder canonicalHeaders = new StringBuilder(); // 构造请求头,多个规范化消息头,按照消息头名称(小写)的字符代码顺序以升序排列后拼接在一起
StringBuilder signedHeadersBuilder = new StringBuilder(); // 已签名消息头列表,多个请求头名称(小写)按首字母升序排列并以英文分号(;)分隔
headers.entrySet().stream().filter(entry -> entry.getKey().toLowerCase().startsWith("x-acs-")
|| entry.getKey().equalsIgnoreCase("host")
|| entry.getKey().equalsIgnoreCase("content-type"))
|| "host".equalsIgnoreCase(entry.getKey())
|| "content-type".equalsIgnoreCase(entry.getKey()))
.sorted(Map.Entry.comparingByKey()).forEach(entry -> {
String lowerKey = entry.getKey().toLowerCase();
canonicalHeaders.append(lowerKey).append(":").append(String.valueOf(entry.getValue()).trim()).append("\n");
@@ -157,13 +161,13 @@ public class AliyunSmsClient extends AbstractSmsClient {
});
String signedHeaders = signedHeadersBuilder.substring(0, signedHeadersBuilder.length() - 1);
// 3. 请求 Body
String requestBody = ""; // 短信 API 为 RPC 接口query parameters 在 uri 中拼接,因此 request body 如果没有特殊要求,设置为空。
String hashedRequestBody = DigestUtil.sha256Hex(requestBody);
// 4. 构建 Authorization 签名
String canonicalRequest = "POST" + "\n" + "/" + "\n" + queryString + "\n"
+ canonicalHeaders + "\n" + signedHeaders + "\n" + hashedRequestBody;
String canonicalRequest = "POST" + "\n" +
"/" + "\n" +
queryString + "\n" +
canonicalHeaders + "\n" +
signedHeaders + "\n" +
hashedRequestPayload;
String hashedCanonicalRequest = DigestUtil.sha256Hex(canonicalRequest);
String stringToSign = "ACS3-HMAC-SHA256" + "\n" + hashedCanonicalRequest;
String signature = SecureUtil.hmacSha256(properties.getApiSecret()).digestHex(stringToSign); // 计算签名
@@ -190,4 +194,4 @@ public class AliyunSmsClient extends AbstractSmsClient {
.replace("%7E", "~"); // 波浪号 "%7E" 被替换为 "~"
}
}
}

View File

@@ -119,6 +119,7 @@ public class TencentSmsClient extends AbstractSmsClient {
return new SmsReceiveRespDTO()
.setSuccess("SUCCESS".equals(statusObj.getStr("report_status"))) // 是否接收成功
.setErrorCode(statusObj.getStr("errmsg")) // 状态报告编码
.setErrorMsg(statusObj.getStr("description")) // 状态报告描述
.setMobile(statusObj.getStr("mobile")) // 手机号
.setReceiveTime(statusObj.getLocalDateTime("user_receive_time", null)) // 状态报告时间
.setSerialNo(statusObj.getStr("sid")); // 发送序列号

View File

@@ -5,6 +5,9 @@ import lombok.Data;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.util.Collection;
import java.util.List;
/**
* 邮箱发送消息
*
@@ -21,8 +24,16 @@ public class MailSendMessage {
/**
* 接收邮件地址
*/
@NotNull(message = "接收邮件地址不能为空")
private String mail;
@NotEmpty(message = "接收邮件地址不能为空")
private Collection<String> toMails;
/**
* 抄送邮件地址
*/
private Collection<String> ccMails;
/**
* 密送邮件地址
*/
private Collection<String> bccMails;
/**
* 邮件账号编号
*/

View File

@@ -7,6 +7,11 @@ import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.Collection;
import java.util.List;
import static java.util.Collections.singletonList;
/**
* Mail 邮件相关消息的 Producer
*
@@ -24,17 +29,22 @@ public class MailProducer {
* 发送 {@link MailSendMessage} 消息
*
* @param sendLogId 发送日志编码
* @param mail 接收邮件地址
* @param toMails 接收邮件地址
* @param ccMails 抄送邮件地址
* @param bccMails 密送邮件地址
* @param accountId 邮件账号编号
* @param nickname 邮件发件人
* @param title 邮件标题
* @param content 邮件内容
* @param nickname 邮件发件人
* @param title 邮件标题
* @param content 邮件内容
*/
public void sendMailSendMessage(Long sendLogId, String mail, Long accountId,
String nickname, String title, String content) {
public void sendMailSendMessage(Long sendLogId,
Collection<String> toMails, Collection<String> ccMails, Collection<String> bccMails,
Long accountId, String nickname, String title, String content) {
MailSendMessage message = new MailSendMessage()
.setLogId(sendLogId).setMail(mail).setAccountId(accountId)
.setNickname(nickname).setTitle(title).setContent(content);
.setLogId(sendLogId)
.setToMails(toMails).setCcMails(ccMails).setBccMails(bccMails)
.setAccountId(accountId).setNickname(nickname)
.setTitle(title).setContent(content);
applicationContext.publishEvent(message);
}

View File

@@ -6,6 +6,7 @@ import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils;
import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils;
import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission;
import cn.iocoder.yudao.module.system.api.logger.dto.LoginLogCreateReqDTO;
import cn.iocoder.yudao.module.system.api.sms.SmsCodeApi;
import cn.iocoder.yudao.module.system.api.sms.dto.code.SmsCodeUseReqDTO;
@@ -97,6 +98,7 @@ public class AdminAuthServiceImpl implements AdminAuthService {
}
@Override
@DataPermission(enable = false)
public AuthLoginRespVO login(AuthLoginReqVO reqVO) {
// 校验验证码
validateCaptcha(reqVO);

View File

@@ -36,6 +36,13 @@ public interface DeptService {
*/
void deleteDept(Long id);
/**
* 批量删除部门
*
* @param ids 部门编号数组
*/
void deleteDeptList(List<Long> ids);
/**
* 获得部门信息
*

View File

@@ -88,6 +88,21 @@ public class DeptServiceImpl implements DeptService {
deptMapper.deleteById(id);
}
@Override
@CacheEvict(cacheNames = RedisKeyConstants.DEPT_CHILDREN_ID_LIST,
allEntries = true) // allEntries 清空所有缓存,因为操作一个部门,涉及到多个缓存
public void deleteDeptList(List<Long> ids) {
// 校验是否有子部门
for (Long id : ids) {
if (deptMapper.selectCountByParentId(id) > 0) {
throw exception(DEPT_EXITS_CHILDREN);
}
}
// 批量删除部门
deptMapper.deleteByIds(ids);
}
@VisibleForTesting
void validateDeptExists(Long id) {
if (id == null) {

View File

@@ -6,6 +6,8 @@ import cn.iocoder.yudao.module.system.dal.dataobject.mail.MailAccountDO;
import cn.iocoder.yudao.module.system.dal.dataobject.mail.MailLogDO;
import cn.iocoder.yudao.module.system.dal.dataobject.mail.MailTemplateDO;
import java.util.Collection;
import java.util.List;
import java.util.Map;
/**
@@ -35,18 +37,21 @@ public interface MailLogService {
/**
* 创建邮件日志
*
* @param userId 用户编码
* @param userType 用户类型
* @param toMail 收件人邮件
* @param account 邮件账号信息
* @param template 模版信息
* @param userId 用户编码
* @param userType 用户类型
* @param toMails 收件人邮件
* @param ccMails 收件人邮件
* @param bccMails 收件人邮件
* @param account 邮件账号信息
* @param template 模版信息
* @param templateContent 模版内容
* @param templateParams 模版参数
* @param isSend 是否发送成功
* @param templateParams 模版参数
* @param isSend 是否发送成功
* @return 日志编号
*/
Long createMailLog(Long userId, Integer userType, String toMail,
MailAccountDO account, MailTemplateDO template ,
Long createMailLog(Long userId, Integer userType,
Collection<String> toMails, Collection<String> ccMails, Collection<String> bccMails,
MailAccountDO account, MailTemplateDO template,
String templateContent, Map<String, Object> templateParams, Boolean isSend);
/**

View File

@@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.system.service.mail;
import cn.hutool.core.collection.ListUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.system.controller.admin.mail.vo.log.MailLogPageReqVO;
import cn.iocoder.yudao.module.system.dal.dataobject.mail.MailAccountDO;
@@ -12,8 +13,7 @@ import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.Map;
import java.util.Objects;
import java.util.*;
import static cn.hutool.core.exceptions.ExceptionUtil.getRootCauseMessage;
@@ -41,7 +41,8 @@ public class MailLogServiceImpl implements MailLogService {
}
@Override
public Long createMailLog(Long userId, Integer userType, String toMail,
public Long createMailLog(Long userId, Integer userType,
Collection<String> toMails, Collection<String> ccMails, Collection<String> bccMails,
MailAccountDO account, MailTemplateDO template,
String templateContent, Map<String, Object> templateParams, Boolean isSend) {
MailLogDO.MailLogDOBuilder logDOBuilder = MailLogDO.builder();
@@ -49,7 +50,8 @@ public class MailLogServiceImpl implements MailLogService {
logDOBuilder.sendStatus(Objects.equals(isSend, true) ? MailSendStatusEnum.INIT.getStatus()
: MailSendStatusEnum.IGNORE.getStatus())
// 用户信息
.userId(userId).userType(userType).toMail(toMail)
.userId(userId).userType(userType)
.toMails(ListUtil.toList(toMails)).ccMails(ListUtil.toList(ccMails)).bccMails(ListUtil.toList(bccMails))
.accountId(account.getId()).fromMail(account.getMail())
// 模板相关字段
.templateId(template.getId()).templateCode(template.getCode()).templateNickname(template.getNickname())

View File

@@ -1,7 +1,9 @@
package cn.iocoder.yudao.module.system.service.mail;
import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
import cn.iocoder.yudao.module.system.mq.message.mail.MailSendMessage;
import java.util.Collection;
import java.util.Map;
/**
@@ -15,38 +17,53 @@ public interface MailSendService {
/**
* 发送单条邮件给管理后台的用户
*
* @param mail 邮箱
* @param userId 用户编码
* @param toMails 收件邮箱
* @param ccMails 抄送邮箱
* @param bccMails 密送邮箱
* @param templateCode 邮件模版编码
* @param templateParams 邮件模版参数
* @return 发送日志编号
*/
Long sendSingleMailToAdmin(String mail, Long userId,
String templateCode, Map<String, Object> templateParams);
default Long sendSingleMailToAdmin(Long userId,
Collection<String> toMails, Collection<String> ccMails, Collection<String> bccMails,
String templateCode, Map<String, Object> templateParams) {
return sendSingleMail(toMails, ccMails, bccMails, userId, UserTypeEnum.ADMIN.getValue(),
templateCode, templateParams);
}
/**
* 发送单条邮件给用户 APP 的用户
*
* @param mail 邮箱
* @param userId 用户编码
* @param toMails 收件邮箱
* @param ccMails 抄送邮箱
* @param bccMails 密送邮箱
* @param templateCode 邮件模版编码
* @param templateParams 邮件模版参数
* @return 发送日志编号
*/
Long sendSingleMailToMember(String mail, Long userId,
String templateCode, Map<String, Object> templateParams);
default Long sendSingleMailToMember(Long userId,
Collection<String> toMails, Collection<String> ccMails, Collection<String> bccMails,
String templateCode, Map<String, Object> templateParams) {
return sendSingleMail(toMails, ccMails, bccMails, userId, UserTypeEnum.MEMBER.getValue(),
templateCode, templateParams);
}
/**
* 发送单条邮件给用户
* 发送单条邮件
*
* @param mail 邮箱
* @param userId 用户编码
* @param toMails 收件邮箱
* @param ccMails 抄送邮箱
* @param bccMails 密送邮箱
* @param userId 用户编号
* @param userType 用户类型
* @param templateCode 邮件模版编码
* @param templateParams 邮件模版参数
* @return 发送日志编号
*/
Long sendSingleMail(String mail, Long userId, Integer userType,
Long sendSingleMail(Collection<String> toMails, Collection<String> ccMails, Collection<String> bccMails,
Long userId, Integer userType,
String templateCode, Map<String, Object> templateParams);
/**

View File

@@ -1,5 +1,7 @@
package cn.iocoder.yudao.module.system.service.mail;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Validator;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.mail.MailAccount;
import cn.hutool.extra.mail.MailUtil;
@@ -18,6 +20,8 @@ import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@@ -50,56 +54,67 @@ public class MailSendServiceImpl implements MailSendService {
private MailProducer mailProducer;
@Override
public Long sendSingleMailToAdmin(String mail, Long userId,
String templateCode, Map<String, Object> templateParams) {
// 如果 mail 为空,则加载用户编号对应的邮箱
if (StrUtil.isEmpty(mail)) {
AdminUserDO user = adminUserService.getUser(userId);
if (user != null) {
mail = user.getEmail();
}
}
// 执行发送
return sendSingleMail(mail, userId, UserTypeEnum.ADMIN.getValue(), templateCode, templateParams);
}
@Override
public Long sendSingleMailToMember(String mail, Long userId,
String templateCode, Map<String, Object> templateParams) {
// 如果 mail 为空,则加载用户编号对应的邮箱
if (StrUtil.isEmpty(mail)) {
mail = memberService.getMemberUserEmail(userId);
}
// 执行发送
return sendSingleMail(mail, userId, UserTypeEnum.MEMBER.getValue(), templateCode, templateParams);
}
@Override
public Long sendSingleMail(String mail, Long userId, Integer userType,
public Long sendSingleMail(Collection<String> toMails, Collection<String> ccMails, Collection<String> bccMails,
Long userId, Integer userType,
String templateCode, Map<String, Object> templateParams) {
// 校验邮箱模版是否合法
// 1.1 校验邮箱模版是否合法
MailTemplateDO template = validateMailTemplate(templateCode);
// 校验邮箱账号是否合法
// 1.2 校验邮箱账号是否合法
MailAccountDO account = validateMailAccount(template.getAccountId());
// 校验邮箱是否存在
mail = validateMail(mail);
// 1.3 校验邮件参数是否缺失
validateTemplateParams(template, templateParams);
// 2. 组装邮箱
String userMail = getUserMail(userId, userType);
Collection<String> toMailSet = new LinkedHashSet<>();
Collection<String> ccMailSet = new LinkedHashSet<>();
Collection<String> bccMailSet = new LinkedHashSet<>();
if (Validator.isEmail(userMail)) {
toMailSet.add(userMail);
}
if (CollUtil.isNotEmpty(toMails)) {
toMails.stream().filter(Validator::isEmail).forEach(toMailSet::add);
}
if (CollUtil.isNotEmpty(ccMails)) {
ccMails.stream().filter(Validator::isEmail).forEach(ccMailSet::add);
}
if (CollUtil.isNotEmpty(bccMails)) {
bccMails.stream().filter(Validator::isEmail).forEach(bccMailSet::add);
}
if (CollUtil.isEmpty(toMailSet)) {
throw exception(MAIL_SEND_MAIL_NOT_EXISTS);
}
// 创建发送日志。如果模板被禁用,则不发送短信,只记录日志
Boolean isSend = CommonStatusEnum.ENABLE.getStatus().equals(template.getStatus());
String title = mailTemplateService.formatMailTemplateContent(template.getTitle(), templateParams);
String content = mailTemplateService.formatMailTemplateContent(template.getContent(), templateParams);
Long sendLogId = mailLogService.createMailLog(userId, userType, mail,
Long sendLogId = mailLogService.createMailLog(userId, userType, toMailSet, ccMailSet, bccMailSet,
account, template, content, templateParams, isSend);
// 发送 MQ 消息,异步执行发送短信
if (isSend) {
mailProducer.sendMailSendMessage(sendLogId, mail, account.getId(),
template.getNickname(), title, content);
mailProducer.sendMailSendMessage(sendLogId, toMailSet, ccMailSet, bccMailSet,
account.getId(), template.getNickname(), title, content);
}
return sendLogId;
}
private String getUserMail(Long userId, Integer userType) {
if (userId == null || userType == null) {
return null;
}
if (UserTypeEnum.ADMIN.getValue().equals(userType)) {
AdminUserDO user = adminUserService.getUser(userId);
if (user != null) {
return user.getEmail();
}
}
if (UserTypeEnum.MEMBER.getValue().equals(userType)) {
return memberService.getMemberUserEmail(userId);
}
return null;
}
@Override
public void doSendMail(MailSendMessage message) {
// 1. 创建发送账号
@@ -107,7 +122,7 @@ public class MailSendServiceImpl implements MailSendService {
MailAccount mailAccount = buildMailAccount(account, message.getNickname());
// 2. 发送邮件
try {
String messageId = MailUtil.send(mailAccount, message.getMail(),
String messageId = MailUtil.send(mailAccount, message.getToMails(), message.getCcMails(), message.getBccMails(),
message.getTitle(), message.getContent(), true);
// 3. 更新结果(成功)
mailLogService.updateMailSendResult(message.getLogId(), messageId, null);
@@ -147,16 +162,8 @@ public class MailSendServiceImpl implements MailSendService {
return account;
}
@VisibleForTesting
String validateMail(String mail) {
if (StrUtil.isEmpty(mail)) {
throw exception(MAIL_SEND_MAIL_NOT_EXISTS);
}
return mail;
}
/**
* 校验邮件参数是否确实
* 校验邮件参数是否缺失
*
* @param template 邮箱模板
* @param templateParams 参数列表

View File

@@ -86,8 +86,8 @@ public class OAuth2GrantServiceImpl implements OAuth2GrantService {
@Override
public OAuth2AccessTokenDO grantClientCredentials(String clientId, List<String> scopes) {
// TODO 芋艿:项目中使用 OAuth2 解决的是三方应用的授权,内部的 SSO 等问题,所以暂时不考虑 client_credentials 这个场景
throw new UnsupportedOperationException("暂时不支持 client_credentials 授权模式");
// 特殊https://yuanbao.tencent.com/bot/app/share/chat/wFj642xSZHHx
return oauth2TokenService.createAccessToken(0L, UserTypeEnum.ADMIN.getValue(), clientId, scopes);
}
@Override

View File

@@ -197,6 +197,9 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService {
* @return 用户信息
*/
private Map<String, String> buildUserInfo(Long userId, Integer userType) {
if (userId == null || userId <= 0) {
return Collections.emptyMap();
}
if (userType.equals(UserTypeEnum.ADMIN.getValue())) {
AdminUserDO user = adminUserService.getUser(userId);
return MapUtil.builder(LoginUser.INFO_KEY_NICKNAME, user.getNickname())
@@ -205,7 +208,7 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService {
// 注意:目前 Member 暂时不读取,可以按需实现
return Collections.emptyMap();
}
return null;
throw new IllegalArgumentException("未知用户类型:" + userType);
}
private static String generateAccessToken() {

View File

@@ -255,9 +255,6 @@ public class MenuServiceImpl implements MenuService {
return;
}
// 如果 id 为空,说明不用比较是否为相同 id 的菜单
if (id == null) {
throw exception(MENU_NAME_DUPLICATE);
}
if (!menu.getId().equals(id)) {
throw exception(MENU_NAME_DUPLICATE);
}

View File

@@ -12,7 +12,7 @@ import java.util.Map;
* 短信日志 Service 接口
*
* @author zzf
* @date 13:48 2021/3/2
* @since 13:48 2021/3/2
*/
public interface SmsLogService {
@@ -49,12 +49,13 @@ public interface SmsLogService {
* 更新日志的接收结果
*
* @param id 日志编号
* @param apiSerialNo 发送编号
* @param success 是否接收成功
* @param receiveTime 用户接收时间
* @param apiReceiveCode API 接收结果的编码
* @param apiReceiveMsg API 接收结果的说明
*/
void updateSmsReceiveResult(Long id, Boolean success,
void updateSmsReceiveResult(Long id, String apiSerialNo, Boolean success,
LocalDateTime receiveTime, String apiReceiveCode, String apiReceiveMsg);
/**

View File

@@ -63,10 +63,17 @@ public class SmsLogServiceImpl implements SmsLogService {
}
@Override
public void updateSmsReceiveResult(Long id, Boolean success, LocalDateTime receiveTime,
public void updateSmsReceiveResult(Long id, String apiSerialNo, Boolean success, LocalDateTime receiveTime,
String apiReceiveCode, String apiReceiveMsg) {
SmsReceiveStatusEnum receiveStatus = Objects.equals(success, true) ?
SmsReceiveStatusEnum.SUCCESS : SmsReceiveStatusEnum.FAILURE;
if (id == null || id == 0) {
SmsLogDO log = smsLogMapper.selectByApiSerialNo(apiSerialNo);
if (log == null) {
return;
}
id = log.getId();
}
smsLogMapper.updateById(SmsLogDO.builder().id(id).receiveStatus(receiveStatus.getStatus())
.receiveTime(receiveTime).apiReceiveCode(apiReceiveCode).apiReceiveMsg(apiReceiveMsg).build());
}

View File

@@ -184,7 +184,7 @@ public class SmsSendServiceImpl implements SmsSendService {
return;
}
// 更新短信日志的接收结果. 因为量一般不大,所以先使用 for 循环更新
receiveResults.forEach(result -> smsLogService.updateSmsReceiveResult(result.getLogId(),
receiveResults.forEach(result -> smsLogService.updateSmsReceiveResult(result.getLogId(), result.getSerialNo(),
result.getSuccess(), result.getReceiveTime(), result.getErrorCode(), result.getErrorMsg()));
}

View File

@@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.system.service.tenant;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
@@ -102,7 +101,7 @@ public class TenantServiceImpl implements TenantService {
// 校验租户名称是否重复
validTenantNameDuplicate(createReqVO.getName(), null);
// 校验租户域名是否重复
validTenantWebsiteDuplicate(createReqVO.getWebsite(), null);
validTenantWebsiteDuplicate(createReqVO.getWebsites(), null);
// 校验套餐被禁用
TenantPackageDO tenantPackage = tenantPackageService.validTenantPackage(createReqVO.getPackageId());
@@ -148,7 +147,7 @@ public class TenantServiceImpl implements TenantService {
// 校验租户名称是否重复
validTenantNameDuplicate(updateReqVO.getName(), updateReqVO.getId());
// 校验租户域名是否重复
validTenantWebsiteDuplicate(updateReqVO.getWebsite(), updateReqVO.getId());
validTenantWebsiteDuplicate(updateReqVO.getWebsites(), updateReqVO.getId());
// 校验套餐被禁用
TenantPackageDO tenantPackage = tenantPackageService.validTenantPackage(updateReqVO.getPackageId());
@@ -175,21 +174,19 @@ public class TenantServiceImpl implements TenantService {
}
}
private void validTenantWebsiteDuplicate(String website, Long id) {
if (StrUtil.isEmpty(website)) {
private void validTenantWebsiteDuplicate(List<String> websites, Long excludeId) {
if (CollUtil.isEmpty(websites)) {
return;
}
TenantDO tenant = tenantMapper.selectByWebsite(website);
if (tenant == null) {
return;
}
// 如果 id 为空,说明不用比较是否为相同名字的租户
if (id == null) {
throw exception(TENANT_WEBSITE_DUPLICATE, website);
}
if (!tenant.getId().equals(id)) {
throw exception(TENANT_WEBSITE_DUPLICATE, website);
}
websites.forEach(website -> {
List<TenantDO> tenants = tenantMapper.selectListByWebsite(website);
if (excludeId != null) {
tenants.removeIf(tenant -> tenant.getId().equals(excludeId));
}
if (CollUtil.isNotEmpty(tenants)) {
throw exception(TENANT_WEBSITE_DUPLICATE, website);
}
});
}
@Override
@@ -263,7 +260,8 @@ public class TenantServiceImpl implements TenantService {
@Override
public TenantDO getTenantByWebsite(String website) {
return tenantMapper.selectByWebsite(website);
List<TenantDO> tenants = tenantMapper.selectListByWebsite(website);
return CollUtil.getFirst(tenants);
}
@Override

View File

@@ -0,0 +1 @@
cn.iocoder.yudao.module.system.framework.captcha.core.PictureWordCaptchaServiceImpl