Repository: ggggborn/SpringCloud-acimage Branch: main Commit: 32c50a3e1d8b Files: 718 Total size: 1.7 MB Directory structure: gitextract_r94k4dmb/ ├── .gitignore ├── README.md ├── acimage_admin/ │ ├── .gitignore │ ├── pom.xml │ └── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── acimage/ │ │ │ └── admin/ │ │ │ ├── AdminApplication.java │ │ │ ├── config/ │ │ │ │ ├── WebMvcConfig.java │ │ │ │ └── mybatis/ │ │ │ │ ├── CommunityDataSourceConfig.java │ │ │ │ ├── GlobalConfigBean.java │ │ │ │ └── ImageDataSourceConfig.java │ │ │ ├── dao/ │ │ │ │ ├── community/ │ │ │ │ │ ├── CategoryDao.java │ │ │ │ │ ├── CommentDao.java │ │ │ │ │ ├── HomeCarouselDao.java │ │ │ │ │ └── TopicDao.java │ │ │ │ ├── sys/ │ │ │ │ │ ├── ApiDao.java │ │ │ │ │ ├── AuthorizeDao.java │ │ │ │ │ ├── PermissionDao.java │ │ │ │ │ ├── RoleDao.java │ │ │ │ │ └── UserRoleDao.java │ │ │ │ └── user/ │ │ │ │ ├── UserDao.java │ │ │ │ └── UserPrivacyDao.java │ │ │ ├── global/ │ │ │ │ └── consts/ │ │ │ │ └── ModuleConstants.java │ │ │ ├── model/ │ │ │ │ ├── request/ │ │ │ │ │ ├── AdminLoginReq.java │ │ │ │ │ ├── ApiAddReq.java │ │ │ │ │ ├── ApiModifyReq.java │ │ │ │ │ ├── ApiQueryReq.java │ │ │ │ │ ├── CarouselAddReq.java │ │ │ │ │ ├── CarouselModifyReq.java │ │ │ │ │ ├── CommentQueryReq.java │ │ │ │ │ ├── PermissionAddReq.java │ │ │ │ │ ├── PermissionModifyReq.java │ │ │ │ │ ├── RoleAddReq.java │ │ │ │ │ ├── RoleModifyReq.java │ │ │ │ │ ├── TopicQueryReq.java │ │ │ │ │ └── UserQueryReq.java │ │ │ │ └── vo/ │ │ │ │ └── WebsiteDataVo.java │ │ │ ├── service/ │ │ │ │ ├── api/ │ │ │ │ │ ├── ApiQueryService.java │ │ │ │ │ ├── ApiWriteService.java │ │ │ │ │ └── impl/ │ │ │ │ │ ├── ApiQueryServiceImpl.java │ │ │ │ │ └── ApiWriteServiceImpl.java │ │ │ │ ├── authorize/ │ │ │ │ │ ├── AuthorizeQueryService.java │ │ │ │ │ ├── AuthorizeWriteService.java │ │ │ │ │ └── impl/ │ │ │ │ │ ├── AuthorizeQueryServiceImpl.java │ │ │ │ │ └── AuthorizeWriteServiceImpl.java │ │ │ │ ├── category/ │ │ │ │ │ ├── CategoryQueryService.java │ │ │ │ │ └── impl/ │ │ │ │ │ └── CategoryQueryServiceImpl.java │ │ │ │ ├── comment/ │ │ │ │ │ ├── CommentQueryService.java │ │ │ │ │ ├── CommentWriteService.java │ │ │ │ │ └── impl/ │ │ │ │ │ ├── CommentQueryServiceImpl.java │ │ │ │ │ └── CommentWriteServiceImpl.java │ │ │ │ ├── homecarousel/ │ │ │ │ │ ├── HomeCarouselQueryService.java │ │ │ │ │ ├── HomeCarouselWriteService.java │ │ │ │ │ └── impl/ │ │ │ │ │ ├── HomeCarouselQueryServiceImpl.java │ │ │ │ │ └── HomeCarouselWriteServiceImpl.java │ │ │ │ ├── login/ │ │ │ │ │ ├── LoginService.java │ │ │ │ │ ├── VerifyCodeService.java │ │ │ │ │ └── impl/ │ │ │ │ │ ├── LoginServiceImpl.java │ │ │ │ │ └── VerifyCodeServiceImpl.java │ │ │ │ ├── permission/ │ │ │ │ │ ├── PermissionQueryService.java │ │ │ │ │ ├── PermissionWriteSercice.java │ │ │ │ │ └── impl/ │ │ │ │ │ ├── PermissionQueryServiceImpl.java │ │ │ │ │ └── PermissionWriteServiceImpl.java │ │ │ │ ├── role/ │ │ │ │ │ ├── RoleQueryService.java │ │ │ │ │ ├── RoleWriteService.java │ │ │ │ │ └── impl/ │ │ │ │ │ ├── RoleQueryServiceImpl.java │ │ │ │ │ └── RoleWriteServiceImpl.java │ │ │ │ ├── topic/ │ │ │ │ │ ├── TopicQueryService.java │ │ │ │ │ ├── TopicWriteService.java │ │ │ │ │ └── impl/ │ │ │ │ │ ├── TopicQueryServiceImpl.java │ │ │ │ │ └── TopicWriteServiceImpl.java │ │ │ │ ├── user/ │ │ │ │ │ ├── UserQueryService.java │ │ │ │ │ ├── UserWriteService.java │ │ │ │ │ └── impl/ │ │ │ │ │ ├── UserQueryServiceImpl.java │ │ │ │ │ └── UserWriteServiceImpl.java │ │ │ │ ├── userrole/ │ │ │ │ │ ├── UserRoleQueryService.java │ │ │ │ │ ├── UserRoleWriteService.java │ │ │ │ │ └── impl/ │ │ │ │ │ ├── UserRoleQueryServiceImpl.java │ │ │ │ │ └── UserRoleWriteServiceImpl.java │ │ │ │ └── websitedata/ │ │ │ │ ├── WebsiteDataService.java │ │ │ │ └── impl/ │ │ │ │ └── WebsiteDataServiceImpl.java │ │ │ └── web/ │ │ │ └── controller/ │ │ │ ├── AdminLoginController.java │ │ │ ├── ApiOperateController.java │ │ │ ├── ApiQueryController.java │ │ │ ├── AuthorizeOperateController.java │ │ │ ├── AuthorizeQueryController.java │ │ │ ├── CategoryController.java │ │ │ ├── CommentOperateController.java │ │ │ ├── CommentQueryController.java │ │ │ ├── HomeCarouselController.java │ │ │ ├── PermissionOperateController.java │ │ │ ├── PermissionQueryController.java │ │ │ ├── RoleController.java │ │ │ ├── TopicOperateController.java │ │ │ ├── TopicQueryController.java │ │ │ ├── UserQueryController.java │ │ │ ├── UserRoleOperateController.java │ │ │ └── WebsiteDataController.java │ │ └── resources/ │ │ ├── application-dev.yml │ │ ├── application.yml │ │ ├── logback-spring.xml │ │ └── mapper/ │ │ ├── community/ │ │ │ └── TopicMapper.xml │ │ ├── image/ │ │ │ └── HomeCarouselMapper.xml │ │ ├── sys/ │ │ │ ├── ApiMapper.xml │ │ │ └── PermissionMapper.xml │ │ └── user/ │ │ └── TopicMapper.xml │ └── test/ │ └── java/ │ └── com/ │ └── acimage/ │ └── admin/ │ ├── AdminApplicationTest.java │ ├── dao/ │ │ ├── community/ │ │ │ ├── SpDaoTest.java │ │ │ └── TopicDaoTest.java │ │ └── sys/ │ │ └── ApiDaoTest.java │ └── service/ │ └── HomeCarouselWriteServiceTest.java ├── acimage_common/ │ ├── .gitignore │ ├── lib/ │ │ └── webp-imageio-core-0.1.0.jar │ ├── pom.xml │ └── src/ │ └── main/ │ ├── java/ │ │ └── com/ │ │ └── acimage/ │ │ └── common/ │ │ ├── CommonMain.java │ │ ├── config/ │ │ │ ├── EsConfig.java │ │ │ ├── EsProdConfig.java │ │ │ ├── FilterConfig.java │ │ │ ├── JacksonConfig.java │ │ │ ├── PaginationConfig.java │ │ │ └── RabbitmqConvertConfig.java │ │ ├── deprecated/ │ │ │ ├── IpInterceptorBak.java │ │ │ ├── JwtInterceptorBak.java │ │ │ ├── PermissionInterceptorBak.java │ │ │ ├── QiniuUtils.java │ │ │ ├── QiniuUtilsBak.java │ │ │ ├── UserCommunityStatistic.java │ │ │ ├── annotation/ │ │ │ │ ├── Authentication.java │ │ │ │ └── utils/ │ │ │ │ └── AuthenticationUtils.java │ │ │ └── typehandler/ │ │ │ └── MatchRuleTypeHandler.java │ │ ├── global/ │ │ │ ├── aop/ │ │ │ │ └── LogAdvice.java │ │ │ ├── consts/ │ │ │ │ ├── EsConstants.java │ │ │ │ ├── FileFormatConstants.java │ │ │ │ ├── HeaderKeyConstants.java │ │ │ │ ├── JwtConstants.java │ │ │ │ ├── MqConstants.java │ │ │ │ ├── StorePrefixConstants.java │ │ │ │ ├── SysKeyConstants.java │ │ │ │ └── TimeConstants.java │ │ │ ├── context/ │ │ │ │ └── UserContext.java │ │ │ ├── enums/ │ │ │ │ ├── AuthenticationType.java │ │ │ │ ├── MatchRule.java │ │ │ │ ├── MyHttpMethod.java │ │ │ │ ├── ServiceType.java │ │ │ │ └── TokenStatus.java │ │ │ └── exception/ │ │ │ ├── BaseException.java │ │ │ ├── BusinessException.java │ │ │ ├── NullTokenException.java │ │ │ └── SystemException.java │ │ ├── model/ │ │ │ ├── Index/ │ │ │ │ └── TopicIndex.java │ │ │ ├── domain/ │ │ │ │ ├── community/ │ │ │ │ │ ├── Category.java │ │ │ │ │ ├── CmtyUser.java │ │ │ │ │ ├── Comment.java │ │ │ │ │ ├── HomeCarousel.java │ │ │ │ │ ├── Star.java │ │ │ │ │ ├── Tag.java │ │ │ │ │ ├── TagTopic.java │ │ │ │ │ ├── Topic.java │ │ │ │ │ └── TopicHtml.java │ │ │ │ ├── image/ │ │ │ │ │ ├── Image.java │ │ │ │ │ └── ImageHash.java │ │ │ │ ├── sys/ │ │ │ │ │ ├── Api.java │ │ │ │ │ ├── Authorize.java │ │ │ │ │ ├── Permission.java │ │ │ │ │ ├── Role.java │ │ │ │ │ └── UserRole.java │ │ │ │ └── user/ │ │ │ │ ├── CommentMsg.java │ │ │ │ ├── User.java │ │ │ │ ├── UserMsg.java │ │ │ │ └── UserPrivacy.java │ │ │ ├── mq/ │ │ │ │ └── dto/ │ │ │ │ ├── EsAddDto.java │ │ │ │ ├── EsDeleteDto.java │ │ │ │ ├── EsUpdateByIdDto.java │ │ │ │ ├── EsUpdateByTermDto.java │ │ │ │ ├── ImageIdWithUrl.java │ │ │ │ ├── ObjectWithClass.java │ │ │ │ ├── SyncImagesUpdateDto.java │ │ │ │ ├── UserIdWithPhotoUrl.java │ │ │ │ └── UserIdWithUsername.java │ │ │ └── page/ │ │ │ └── MyPage.java │ │ ├── redis/ │ │ │ ├── QueryRedisAdvice.java │ │ │ ├── RequestLimitAdvice.java │ │ │ ├── annotation/ │ │ │ │ ├── KeyParam.java │ │ │ │ ├── QueryRedis.java │ │ │ │ └── RequestLimit.java │ │ │ ├── enums/ │ │ │ │ ├── DataType.java │ │ │ │ └── LimitTarget.java │ │ │ └── utils/ │ │ │ └── RedisAnnotationUtils.java │ │ ├── result/ │ │ │ ├── Code.java │ │ │ └── Result.java │ │ ├── service/ │ │ │ ├── TokenService.java │ │ │ └── impl/ │ │ │ └── TokenServiceImpl.java │ │ ├── utils/ │ │ │ ├── CookieUtils.java │ │ │ ├── EsUtils.java │ │ │ ├── ExceptionUtils.java │ │ │ ├── HtmlUtils.java │ │ │ ├── IdGenerator.java │ │ │ ├── ImageUtils.java │ │ │ ├── IpUtils.java │ │ │ ├── JwtUtils.java │ │ │ ├── LambdaUtils.java │ │ │ ├── RsaUtils.java │ │ │ ├── SensitiveWordUtils.java │ │ │ ├── SpringContextUtils.java │ │ │ ├── common/ │ │ │ │ ├── AopUtils.java │ │ │ │ ├── ArrayUtils.java │ │ │ │ ├── BeanUtils.java │ │ │ │ ├── FileUtils.java │ │ │ │ ├── JacksonUtils.java │ │ │ │ ├── ListUtils.java │ │ │ │ ├── PageUtils.java │ │ │ │ ├── PairUtils.java │ │ │ │ ├── ReflectUtils.java │ │ │ │ └── StringUtils.java │ │ │ ├── minio/ │ │ │ │ ├── MinioProperties.java │ │ │ │ └── MinioUtils.java │ │ │ └── redis/ │ │ │ ├── RedisLuaUtils.java │ │ │ └── RedisUtils.java │ │ └── web/ │ │ ├── exceptionhandler/ │ │ │ ├── ArgumentValidateExceptionHandler.java │ │ │ ├── GlobalExceptionHandler.java │ │ │ └── JwtExceptionHandler.java │ │ └── interceptor/ │ │ ├── AccessInterceptor.java │ │ └── JwtInterceptor.java │ └── resources/ │ ├── application-common.yml │ ├── application-qiniu-template.yml │ ├── lua/ │ │ ├── getAndCombineAndDelete.lua │ │ ├── incrementIfPresent.lua │ │ ├── incrementIfPresentForFieldKey.lua │ │ ├── incrementIfPresentForZSet.lua │ │ ├── requestLimit.lua │ │ └── setIfPresentForFieldKey.lua │ └── sensitive_word.txt ├── acimage_community/ │ ├── lib/ │ │ └── webp-imageio-core-0.1.0.jar │ ├── pom.xml │ └── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── acimage/ │ │ │ └── community/ │ │ │ ├── CommunityApplication.java │ │ │ ├── dao/ │ │ │ │ ├── CategoryDao.java │ │ │ │ ├── CmtyUserDao.java │ │ │ │ ├── CommentDao.java │ │ │ │ ├── HomeCarrouselDao.java │ │ │ │ ├── ImageDao.java │ │ │ │ ├── StarDao.java │ │ │ │ ├── TagDao.java │ │ │ │ ├── TagTopicDao.java │ │ │ │ ├── TopicDao.java │ │ │ │ └── TopicHtmlDao.java │ │ │ ├── depreted/ │ │ │ │ ├── CmtyUserDaoBak.java │ │ │ │ ├── RabbitmqConvertConfig.java │ │ │ │ ├── UserMixWriteService.java │ │ │ │ ├── UserMixWriteServiceImpl.java │ │ │ │ └── userstatistic/ │ │ │ │ ├── UserCsQueryService.java │ │ │ │ ├── UserCsRankService.java │ │ │ │ ├── UserCsWriteService.java │ │ │ │ ├── consts/ │ │ │ │ │ └── KeyConstants.java │ │ │ │ └── impl/ │ │ │ │ ├── UserCsQueryServiceImpl.java │ │ │ │ ├── UserCsRankServiceImpl.java │ │ │ │ └── UserCsWriteServiceImpl.java │ │ │ ├── esdao/ │ │ │ │ └── UserEsDao.java │ │ │ ├── global/ │ │ │ │ ├── annotation/ │ │ │ │ │ ├── RecordPageView.java │ │ │ │ │ └── TopicId.java │ │ │ │ ├── aop/ │ │ │ │ │ └── RecordPageViewAdvice.java │ │ │ │ ├── config/ │ │ │ │ │ ├── JobFactory.java │ │ │ │ │ └── WebMvcConfig.java │ │ │ │ ├── consts/ │ │ │ │ │ ├── CommentKeyConstants.java │ │ │ │ │ ├── CoverImageConstants.java │ │ │ │ │ ├── PageSizeConstants.java │ │ │ │ │ ├── StarKeyConstants.java │ │ │ │ │ └── TopicKeyConstants.java │ │ │ │ └── enums/ │ │ │ │ ├── SortMode.java │ │ │ │ └── TopicAttribute.java │ │ │ ├── listener/ │ │ │ │ ├── CommentEventListener.java │ │ │ │ ├── PublishTopicEventListener.java │ │ │ │ ├── StarEventListener.java │ │ │ │ └── event/ │ │ │ │ ├── CommentEvent.java │ │ │ │ ├── StarEvent.java │ │ │ │ └── TopicEvent.java │ │ │ ├── model/ │ │ │ │ ├── request/ │ │ │ │ │ ├── CommentAddReq.java │ │ │ │ │ ├── CommentModifyReq.java │ │ │ │ │ ├── TopicAddReq.java │ │ │ │ │ ├── TopicModifyHtmlReq.java │ │ │ │ │ ├── TopicQueryByCategoryIdReq.java │ │ │ │ │ ├── TopicQueryBySortReq.java │ │ │ │ │ ├── TopicQueryByTagIdReq.java │ │ │ │ │ ├── TopicSearchReq.java │ │ │ │ │ ├── UserLoginReq.java │ │ │ │ │ └── UserRegisterReq.java │ │ │ │ └── vo/ │ │ │ │ └── TopicInfoVo.java │ │ │ ├── mq/ │ │ │ │ ├── config/ │ │ │ │ │ ├── SyncEsMqConfig.java │ │ │ │ │ ├── SyncImagesMqConfig.java │ │ │ │ │ └── UserMsgMqConfig.java │ │ │ │ ├── consumer/ │ │ │ │ │ ├── SyncEsConsumer.java │ │ │ │ │ └── SyncUserConsumer.java │ │ │ │ └── producer/ │ │ │ │ ├── SyncEsMqProducer.java │ │ │ │ ├── UserMsgMqProducer.java │ │ │ │ └── syncImagesMqProducer.java │ │ │ ├── runner/ │ │ │ │ ├── CreateIndexRunner.java │ │ │ │ └── PreheatApplicationRunner.java │ │ │ ├── schedule/ │ │ │ │ ├── UpdateActivityTimeJob.java │ │ │ │ ├── UpdateActivityTimeJobConfig.java │ │ │ │ ├── UpdateCommentCountJob.java │ │ │ │ ├── UpdateCommentCountJobConfig.java │ │ │ │ ├── UpdatePageViewJob.java │ │ │ │ ├── UpdatePageViewJobConfig.java │ │ │ │ ├── UpdateStarCountJob.java │ │ │ │ └── UpdateStarCountJobConfig.java │ │ │ ├── service/ │ │ │ │ ├── categoty/ │ │ │ │ │ ├── CategoryQueryService.java │ │ │ │ │ └── impl/ │ │ │ │ │ └── CategoryQueryServiceImpl.java │ │ │ │ ├── cmtyuser/ │ │ │ │ │ ├── CmtyUserQueryService.java │ │ │ │ │ ├── CmtyUserRankService.java │ │ │ │ │ ├── CmtyUserWriteService.java │ │ │ │ │ └── impl/ │ │ │ │ │ ├── CmtyUserQueryServiceImpl.java │ │ │ │ │ ├── CmtyUserRankServiceImpl.java │ │ │ │ │ └── CmtyUserWriteServiceImpl.java │ │ │ │ ├── comment/ │ │ │ │ │ ├── CommentInfoQueryService.java │ │ │ │ │ ├── CommentQueryService.java │ │ │ │ │ ├── CommentWriteService.java │ │ │ │ │ ├── annotation/ │ │ │ │ │ │ ├── Operation.java │ │ │ │ │ │ └── UpdateCcByReturn.java │ │ │ │ │ └── impl/ │ │ │ │ │ ├── CommentInfoQueryServiceImpl.java │ │ │ │ │ ├── CommentQueryServiceImpl.java │ │ │ │ │ └── CommentWriteServiceImpl.java │ │ │ │ ├── homecarousel/ │ │ │ │ │ ├── HomeCarouselQueryService.java │ │ │ │ │ └── impl/ │ │ │ │ │ └── HomeCarouselQueryServiceImpl.java │ │ │ │ ├── star/ │ │ │ │ │ ├── StarMixQueryService.java │ │ │ │ │ ├── StarQueryService.java │ │ │ │ │ ├── StarWriteService.java │ │ │ │ │ └── impl/ │ │ │ │ │ ├── StarMixQueryServiceImpl.java │ │ │ │ │ ├── StarQueryServiceImpl.java │ │ │ │ │ └── StarWriteServiceImpl.java │ │ │ │ ├── tag/ │ │ │ │ │ ├── TagQueryService.java │ │ │ │ │ ├── TagTopicQueryService.java │ │ │ │ │ ├── TagTopicWriteService.java │ │ │ │ │ ├── TagWriteService.java │ │ │ │ │ └── impl/ │ │ │ │ │ ├── TagQueryServiceImpl.java │ │ │ │ │ ├── TagTopicQueryServiceImpl.java │ │ │ │ │ ├── TagTopicWriteServiceImpl.java │ │ │ │ │ └── TagWriteServiceImpl.java │ │ │ │ └── topic/ │ │ │ │ ├── Impl/ │ │ │ │ │ ├── TopicEsSearchServiceImpl.java │ │ │ │ │ ├── TopicHtmlQueryServiceImpl.java │ │ │ │ │ ├── TopicHtmlWriteServiceImpl.java │ │ │ │ │ ├── TopicInfoQueryServiceImpl.java │ │ │ │ │ ├── TopicInfoWriteServiceImpl.java │ │ │ │ │ ├── TopicPreheatServiceImpl.java │ │ │ │ │ ├── TopicQueryServiceImpl.java │ │ │ │ │ ├── TopicRankQueryServiceImpl.java │ │ │ │ │ ├── TopicRankWriteServiceImpl.java │ │ │ │ │ ├── TopicSpAttrQueryServiceImpl.java │ │ │ │ │ ├── TopicSpAttrWriteServiceImpl.java │ │ │ │ │ └── TopicWriteServiceImpl.java │ │ │ │ ├── TopicEsSearchService.java │ │ │ │ ├── TopicHtmlQueryService.java │ │ │ │ ├── TopicHtmlWriteService.java │ │ │ │ ├── TopicInfoQueryService.java │ │ │ │ ├── TopicInfoWriteService.java │ │ │ │ ├── TopicPreheatService.java │ │ │ │ ├── TopicQueryService.java │ │ │ │ ├── TopicRankQueryService.java │ │ │ │ ├── TopicRankWriteService.java │ │ │ │ ├── TopicSpAttrQueryService.java │ │ │ │ ├── TopicSpAttrWriteService.java │ │ │ │ └── TopicWriteService.java │ │ │ ├── utils/ │ │ │ │ └── RsaUtils.java │ │ │ └── web/ │ │ │ ├── controller/ │ │ │ │ ├── CategoryQueryController.java │ │ │ │ ├── CommentOperateController.java │ │ │ │ ├── CommentQueryController.java │ │ │ │ ├── HomeCarouselQueryController.java │ │ │ │ ├── StarOperateController.java │ │ │ │ ├── StarQueryController.java │ │ │ │ ├── TagQueryController.java │ │ │ │ ├── TopicOperateController.java │ │ │ │ ├── TopicQueryController.java │ │ │ │ ├── TopicSearchController.java │ │ │ │ └── UserRankController.java │ │ │ └── provider/ │ │ │ ├── CmtyUserProvider.java │ │ │ ├── CommentProvider.java │ │ │ └── TopicProvider.java │ │ └── resources/ │ │ ├── application-dev.yml │ │ ├── application.yml │ │ ├── logback-spring.xml │ │ └── mapper/ │ │ ├── CmtyUserMapper.xml │ │ ├── CommentMapper.xml │ │ ├── ImageMapper.xml │ │ ├── StarMapper.xml │ │ ├── TagTopicMapper.xml │ │ └── TopicMapper.xml │ └── test/ │ └── java/ │ └── com/ │ └── acimage/ │ └── community/ │ ├── CasualTest.java │ ├── CommunityApplicationTests.java │ ├── dao/ │ │ ├── CommentDaoTest.java │ │ ├── ImageDaoTest.java │ │ ├── StarDaoTest.java │ │ └── TopicDaoTest.java │ ├── service/ │ │ ├── CommentWriteServiceTest.java │ │ └── TopicEsSearchServiceTest.java │ └── utils/ │ └── RedisTest.java ├── acimage_feign/ │ ├── pom.xml │ └── src/ │ └── main/ │ └── java/ │ └── com/ │ └── acimage/ │ └── feign/ │ ├── FeignMain.java │ ├── client/ │ │ ├── CmtyUserClient.java │ │ ├── CommentClient.java │ │ ├── TopicClient.java │ │ └── UserClient.java │ ├── config/ │ │ ├── FallbackFactoryBean.java │ │ ├── FeignMultipartSupportConfig.java │ │ └── FeignRequestInterceptorConfig.java │ ├── depreted/ │ │ ├── FileClient.java │ │ └── ImageClient.java │ └── fallback/ │ ├── CmtyUserClientFallbackFactory.java │ ├── CommentClientFallbackFactory.java │ ├── ImageClientFallbackFactory.java │ ├── TopicClientFallbackFactory.java │ └── UserClientFallbackFactory.java ├── acimage_gateway/ │ ├── pom.xml │ └── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── acimage/ │ │ │ └── gateway/ │ │ │ ├── GatewayApplication.java │ │ │ ├── apitree/ │ │ │ │ ├── ApiTree.java │ │ │ │ ├── ApiTreeFactory.java │ │ │ │ ├── ApiTreeUtils.java │ │ │ │ └── InitApiTreeApplicationRunner.java │ │ │ ├── config/ │ │ │ │ ├── KeySolverConfig.java │ │ │ │ └── RoleConfig.java │ │ │ ├── dao/ │ │ │ │ ├── ApiDao.java │ │ │ │ ├── AuthorizeDao.java │ │ │ │ ├── PermissionDao.java │ │ │ │ ├── RoleDao.java │ │ │ │ └── UserRoleDao.java │ │ │ ├── global/ │ │ │ │ └── consts/ │ │ │ │ └── NotationConstants.java │ │ │ ├── globalfilter/ │ │ │ │ ├── AuthenticationFilter.java │ │ │ │ ├── CustomWebsocketRoutingFilter.java │ │ │ │ ├── PermissionFilter.java │ │ │ │ ├── RemoveContextFilter.java │ │ │ │ └── RequestLimitFilter.java │ │ │ ├── schedule/ │ │ │ │ └── RefreshApiTreeSchedule.java │ │ │ └── serivce/ │ │ │ ├── ApiQueryService.java │ │ │ ├── AuthorizeQueryService.java │ │ │ ├── RoleQueryService.java │ │ │ ├── UserRoleQueryService.java │ │ │ └── impl/ │ │ │ ├── ApiQueryQueryServiceImpl.java │ │ │ ├── AuthorizeQueryServiceImpl.java │ │ │ ├── RoleQueryServiceImpl.java │ │ │ └── UserRoleQueryQueryServiceImpl.java │ │ └── resources/ │ │ ├── application-dev.yml │ │ ├── application.yml │ │ └── logback-spring.xml │ └── test/ │ └── java/ │ └── com/ │ └── acimage/ │ └── gateway/ │ ├── GatewayApplicationTest.java │ └── apitree/ │ └── ApiTreeTest.java ├── acimage_image/ │ ├── lib/ │ │ └── webp-imageio-core-0.1.0.jar │ ├── pom.xml │ └── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── acimage/ │ │ │ └── image/ │ │ │ ├── ImageApplication.java │ │ │ ├── dao/ │ │ │ │ ├── ImageDao.java │ │ │ │ └── ImageHashDao.java │ │ │ ├── global/ │ │ │ │ ├── consts/ │ │ │ │ │ ├── MyFileConstants.java │ │ │ │ │ └── TopicImageKeyConstants.java │ │ │ │ └── context/ │ │ │ │ └── DirectoryContext.java │ │ │ ├── mq/ │ │ │ │ └── consumer/ │ │ │ │ └── SyncImagesConsumer.java │ │ │ ├── service/ │ │ │ │ ├── config/ │ │ │ │ │ └── DhashTaskPoolConfig.java │ │ │ │ ├── image/ │ │ │ │ │ ├── ImageMixWriteService.java │ │ │ │ │ ├── ImageQueryService.java │ │ │ │ │ ├── ImageWriteService.java │ │ │ │ │ └── impl/ │ │ │ │ │ ├── ImageMixWriteServiceImpl.java │ │ │ │ │ ├── ImageQueryServiceImpl.java │ │ │ │ │ └── ImageWriteServiceImpl.java │ │ │ │ ├── imagehash/ │ │ │ │ │ ├── ImageHashWriteService.java │ │ │ │ │ ├── SearchImageService.java │ │ │ │ │ └── impl/ │ │ │ │ │ ├── ImageHashWriteServiceImpl.java │ │ │ │ │ └── SearchImageServiceImpl.java │ │ │ │ └── photo/ │ │ │ │ ├── PhotoService.java │ │ │ │ └── impl/ │ │ │ │ └── PhotoServiceImpl.java │ │ │ ├── utils/ │ │ │ │ ├── BitUtils.java │ │ │ │ ├── DhashUtils.java │ │ │ │ ├── ImageFileUtils.java │ │ │ │ └── ImageUtils.java │ │ │ └── web/ │ │ │ ├── config/ │ │ │ │ └── WebMvcConfig.java │ │ │ ├── controller/ │ │ │ │ ├── ImageOperateController.java │ │ │ │ ├── PhotoOperateController.java │ │ │ │ └── SearchImageController.java │ │ │ └── provider/ │ │ │ └── ImageProvider.java │ │ └── resources/ │ │ ├── application-dev.yml │ │ ├── application.yml │ │ ├── logback-spring.xml │ │ └── mapper/ │ │ └── ImageMapper.xml │ └── test/ │ └── java/ │ └── com/ │ └── acimage/ │ └── image/ │ ├── CasualTest.java │ ├── ImageApplicationTests.java │ ├── dao/ │ │ ├── ImageDaoTest.java │ │ └── ImageHashDaoDaoTest.java │ ├── service/ │ │ ├── FileServiceTest.java │ │ └── SearchImageServiceTest.java │ └── utils/ │ └── ImageHashDaoUtilsTest.java ├── acimage_user/ │ ├── pom.xml │ └── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── acimage/ │ │ │ └── user/ │ │ │ ├── UserApplication.java │ │ │ ├── dao/ │ │ │ │ ├── CommentMsgDao.java │ │ │ │ ├── UserDao.java │ │ │ │ ├── UserMsgDao.java │ │ │ │ └── UserPrivacyDao.java │ │ │ ├── global/ │ │ │ │ ├── config/ │ │ │ │ │ ├── WebMvcConfig.java │ │ │ │ │ └── WebSocketConfig.java │ │ │ │ └── consts/ │ │ │ │ ├── PageSizeConsts.java │ │ │ │ ├── StorePrefixConst.java │ │ │ │ └── WebSocketSessionConstants.java │ │ │ ├── model/ │ │ │ │ ├── request/ │ │ │ │ │ ├── UserLoginReq.java │ │ │ │ │ └── UserRegisterReq.java │ │ │ │ └── vo/ │ │ │ │ └── ProfileVo.java │ │ │ ├── mq/ │ │ │ │ ├── config/ │ │ │ │ │ └── SyncUserMqConfig.java │ │ │ │ ├── consumer/ │ │ │ │ │ └── UserMsgConsumer.java │ │ │ │ └── producer/ │ │ │ │ └── SyncUserMqProducer.java │ │ │ ├── service/ │ │ │ │ ├── commentmsg/ │ │ │ │ │ ├── CommentMsgQueryService.java │ │ │ │ │ ├── CommentMsgWriteService.java │ │ │ │ │ └── impl/ │ │ │ │ │ ├── CommentMsgQueryServiceImpl.java │ │ │ │ │ └── CommentMsgWriteServiceImpl.java │ │ │ │ ├── mail/ │ │ │ │ │ ├── MainService.java │ │ │ │ │ └── impl/ │ │ │ │ │ └── MailServiceImpl.java │ │ │ │ ├── user/ │ │ │ │ │ ├── LoginService.java │ │ │ │ │ ├── UserInfoService.java │ │ │ │ │ ├── UserQueryService.java │ │ │ │ │ ├── UserRankService.java │ │ │ │ │ ├── UserWriteService.java │ │ │ │ │ ├── consts/ │ │ │ │ │ │ └── KeyConstants.java │ │ │ │ │ └── impl/ │ │ │ │ │ ├── LoginServiceImpl.java │ │ │ │ │ ├── UserInfoServiceImpl.java │ │ │ │ │ ├── UserQueryServiceImpl.java │ │ │ │ │ ├── UserRankServiceImpl.java │ │ │ │ │ └── UserWriteServiceImpl.java │ │ │ │ ├── usermsg/ │ │ │ │ │ ├── UserMsgQueryService.java │ │ │ │ │ ├── UserMsgWriteService.java │ │ │ │ │ └── impl/ │ │ │ │ │ ├── UserMsgQueryServiceImpl.java │ │ │ │ │ └── UserMsgWriteServiceImpl.java │ │ │ │ └── verify/ │ │ │ │ ├── VerifyCodeService.java │ │ │ │ └── impl/ │ │ │ │ └── VerifyCodeServiceImpl.java │ │ │ └── web/ │ │ │ ├── controller/ │ │ │ │ ├── LoginController.java │ │ │ │ ├── MessageController.java │ │ │ │ ├── UserOperateController.java │ │ │ │ ├── UserQueryController.java │ │ │ │ └── VerifyCodeController.java │ │ │ ├── provider/ │ │ │ │ └── UserProvider.java │ │ │ └── websocket/ │ │ │ ├── MyHandshakeInterceptor.java │ │ │ └── MyWebSocketHandler.java │ │ └── resources/ │ │ ├── application-dev.yml │ │ ├── application.yml │ │ ├── logback-spring.xml │ │ └── mapper/ │ │ ├── CommentMsgMapper.xml │ │ └── UserMapper.xml │ └── test/ │ └── java/ │ └── com/ │ └── acimage/ │ └── user/ │ └── AppTest.java ├── doc/ │ └── sql/ │ ├── .gitignore │ ├── acimage_community.sql │ ├── acimage_image.sql │ ├── acimage_sys.sql │ └── acimage_user.sql ├── pom.xml ├── vue-manage-system/ │ ├── .github/ │ │ └── FUNDING.yml │ ├── .gitignore │ ├── LICENSE │ ├── README.md │ ├── README_EN.md │ ├── auto-imports.d.ts │ ├── components.d.ts │ ├── index.html │ ├── package.json │ ├── public/ │ │ ├── table.json │ │ └── template.xlsx │ ├── src/ │ │ ├── App.vue │ │ ├── api/ │ │ │ ├── HomeCarousel.ts │ │ │ ├── UserRole.ts │ │ │ ├── WebsiteData.ts │ │ │ ├── api.ts │ │ │ ├── authorize.ts │ │ │ ├── category.ts │ │ │ ├── comment.ts │ │ │ ├── index.ts │ │ │ ├── login.ts │ │ │ ├── permission.ts │ │ │ ├── role.ts │ │ │ ├── topic.ts │ │ │ └── user.ts │ │ ├── assets/ │ │ │ └── css/ │ │ │ ├── color-dark.css │ │ │ ├── icon.css │ │ │ └── main.css │ │ ├── components/ │ │ │ ├── header.vue │ │ │ ├── sidebar.vue │ │ │ └── tags.vue │ │ ├── config.ts │ │ ├── main.ts │ │ ├── router/ │ │ │ └── index.ts │ │ ├── store/ │ │ │ ├── permiss.ts │ │ │ ├── sidebar.ts │ │ │ ├── store.ts │ │ │ └── tags.ts │ │ ├── utils/ │ │ │ ├── CommonUtils.ts │ │ │ ├── MessageUtils.ts │ │ │ ├── StringUtils.ts │ │ │ ├── global.ts │ │ │ ├── request.ts │ │ │ ├── requestx.ts │ │ │ ├── result.ts │ │ │ └── utils.js │ │ ├── views/ │ │ │ ├── 403.vue │ │ │ ├── 404.vue │ │ │ ├── HomeCarousel/ │ │ │ │ └── HomeCarousel.vue │ │ │ ├── api/ │ │ │ │ └── api.vue │ │ │ ├── authorize/ │ │ │ │ └── authorize.vue │ │ │ ├── charts.vue │ │ │ ├── comment/ │ │ │ │ └── comment.vue │ │ │ ├── dashboard.vue │ │ │ ├── donate.vue │ │ │ ├── editor.vue │ │ │ ├── home.vue │ │ │ ├── icon.vue │ │ │ ├── login.vue │ │ │ ├── markdown.vue │ │ │ ├── permission/ │ │ │ │ └── permission.vue │ │ │ ├── permissionx.vue │ │ │ ├── role/ │ │ │ │ └── role.vue │ │ │ ├── table.vue │ │ │ ├── tabs.vue │ │ │ ├── topic/ │ │ │ │ └── topic.vue │ │ │ ├── upload.vue │ │ │ ├── user/ │ │ │ │ └── user.vue │ │ │ └── user.vue │ │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts └── vue_acimage_web/ ├── .gitignore ├── README.md ├── babel.config.js ├── jsconfig.json ├── mock/ │ ├── HomeCarousel.js │ ├── UserRank.js │ ├── index.js │ └── topic.js ├── package.json ├── public/ │ ├── index.html │ ├── static/ │ │ └── css/ │ │ └── common.css │ └── tinymce/ │ ├── langs/ │ │ ├── README.md │ │ └── zh-Hans.js │ ├── license.txt │ ├── plugins/ │ │ └── emoticons/ │ │ └── js/ │ │ ├── emojiimages.js │ │ └── emojis.js │ └── tinymce.d.ts ├── src/ │ ├── App.vue │ ├── api/ │ │ ├── HomeCarousel.js │ │ ├── TopicSearch.js │ │ ├── UserRank.js │ │ ├── category.js │ │ ├── comment.js │ │ ├── image.js │ │ ├── login.js │ │ ├── message.js │ │ ├── photo.js │ │ ├── star.js │ │ ├── tag.js │ │ ├── topic.js │ │ └── user.js │ ├── components/ │ │ ├── BaseContainer/ │ │ │ ├── BaseContainer.css │ │ │ └── BaseContainer.vue │ │ ├── CategoryCard/ │ │ │ ├── CategoryCard.css │ │ │ └── CategoryCard.vue │ │ ├── EditBoard/ │ │ │ ├── EditBoard.css │ │ │ └── EditBoard.vue │ │ ├── FloatImage/ │ │ │ ├── FloatImage.css │ │ │ └── FloatImage.vue │ │ ├── HelloWorld.vue │ │ ├── HomeCarousel/ │ │ │ └── HomeCarousel.vue │ │ ├── MaskImage/ │ │ │ ├── MaskImage.css │ │ │ └── MaskImage.vue │ │ ├── MyHeader/ │ │ │ ├── MyHeader.css │ │ │ ├── MyHeader.vue │ │ │ └── MyNavigation/ │ │ │ └── MyNavigation.vue │ │ ├── ShowTinymce/ │ │ │ └── ShowTinymce.vue │ │ ├── TagCard/ │ │ │ ├── TagCard.css │ │ │ └── TagCard.vue │ │ ├── TopicCard/ │ │ │ ├── TopicCard.css │ │ │ └── TopicCard.vue │ │ ├── TopicList/ │ │ │ ├── TopicList.css │ │ │ └── TopicList.vue │ │ └── TopicRank/ │ │ ├── TopicRank.css │ │ └── TopicRank.vue │ ├── config.js │ ├── main.js │ ├── router/ │ │ └── index.js │ ├── store/ │ │ └── index.js │ ├── utils/ │ │ ├── CommonUtils.js │ │ ├── MessageUtils.js │ │ ├── StringUtils.js │ │ ├── global.js │ │ ├── request.js │ │ ├── result.js │ │ └── utils.js │ └── views/ │ ├── About/ │ │ ├── About.css │ │ └── About.vue │ ├── Forum/ │ │ ├── Forum.css │ │ └── Forum.vue │ ├── Home/ │ │ ├── Home.css │ │ ├── Home.vue │ │ └── UserRank/ │ │ ├── UserRank.css │ │ └── UserRank.vue │ ├── Login/ │ │ ├── Login.css │ │ └── Login.vue │ ├── MyActivity/ │ │ ├── MyActivity.css │ │ ├── MyActivity.vue │ │ ├── MyCommentActivity/ │ │ │ ├── MyCommentActivity.css │ │ │ └── MyCommentActivity.vue │ │ ├── MyStarActivity/ │ │ │ ├── MyStarActivity.css │ │ │ └── MyStarActivity.vue │ │ └── MyTopicActivity/ │ │ ├── MyTopicActivity.css │ │ └── MyTopicActivity.vue │ ├── MyMessage/ │ │ ├── CommentMessage/ │ │ │ ├── CommentMessage.css │ │ │ └── CommentMessage.vue │ │ ├── MyMessage.css │ │ └── MyMessage.vue │ ├── MyProfile/ │ │ ├── MyProfile.css │ │ └── MyProfile.vue │ ├── PublishTopic/ │ │ ├── PublishTopic.css │ │ └── PublishTopic.vue │ ├── SearchImage/ │ │ ├── SearchImage.css │ │ └── SearchImage.vue │ ├── SearchTopic/ │ │ ├── SearchTopic.css │ │ └── SearchTopic.vue │ └── TopicInfo/ │ ├── PublishComment/ │ │ ├── PublishComment.css │ │ └── PublishComment.vue │ ├── TopicInfo.css │ ├── TopicInfo.vue │ └── UserComment/ │ ├── UserComment.css │ └── UserComment.vue └── vue.config.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ /.idea target /temp application-dev?.yml application-prod.yml application-prod?.yml application-server.yml application-server?.yml application-common-secret.yml application-qiniu.yml ================================================ FILE: README.md ================================================ # 次元印象

次元印象,一个SpringCloud构建的动漫交流论坛

## 前言 传说Github上的中国程序员有一半是二次元,于是~~为了吸引更多star~~我边学习SpringCloud技术边开发了这个动漫交流论坛。

## 项目介绍 **次元印象** ( **acimage**) 是一个**基于SpringCloud**构建的**前后端分离**的动漫交流论坛。后端使用 **SpringCloud**+**Mybatis-Plus**+**Reids**+**Rabbitmq**+**Elasticsearch**。前端使用 **Vue** + **ElementUi** + **Vite** 。项目已经上线。各位Github的二次元们,还不来点个star(๑•̀ㅂ•́)و✧。 ## 网址 **次元印象o(*≧▽≦)ツ~动漫交流论坛** www.acimage.top (只适配网页端,如果画面显小,可以适当放大浏览器,视觉效果更佳)。 **首页**

**论坛页**

## 系统架构

## 项目功能 #### 用户前台 - **以图识图**,即上传图片,识别出论坛内相似图片及其所在话题。 - **多样化搜索**,可以同时根据关键词、分类、标签以及排序字段等搜索,并根据关键词高亮匹配文字。 - **相关话题推荐**,**随机推荐**。 - **话题排行榜**(按star、浏览量、评论数等、活跃时间排行)。 - **用户排行榜**(按话题数、star等排行)。 - 分类、标签,并且点击可筛选出符合相关分类或标签的话题。 - **实时展示最新活跃话题**。 - **敏感词过滤**,**图片压缩**。 - **消息通知** - 查看个人动态、修改个人信息。 - 登录、注册、登出,**验证码防刷**,**邮箱验证**。 - 发表话题,评论,star等。 #### 后台管理 - 较为完善的RBAC权限控制,可动态调整用户角色、角色权限、接口权限等。 - 网站基本数据展示,访客量,接口调用数等。 - 首页轮播图片管理。 - 话题、评论、用户管理。 - ## 项目特点 - **JWT**实现单点登录,网关统一身份认证和鉴权,完善的**RBAC**权限控制。 - 利用**Redis+Lua脚本+定时任务**对变动比较频繁的数据(浏览量等)定时持久化。 - 利用**Redis+Lua脚本+自定义注解**,只需一个注解就可以实现针对ip、用户、和总体请求量的限流。 - 利用**Dhash**算法实现以图识图功能,效果还可以。 - 上传的图片均用webp压缩,极大地减少带宽压力。 ## 项目目录 **后端服务** - **acimage_gateway**:网关,统一身份认证、鉴权,分别针对用户和总体请求量的限流 - **acimage_user**:用户中心,用户注册、登录,邮箱服务等 - **acimage_community**:社区服务,负责论坛话题相关的核心功能 - **acimage_image**:图片服务,负责话题图片上传、头像上传、以图识图等功能 - **acimage_admin**: 管理服务 **后端模块** - **acimage_common**:公共模块,存放实体类、公共的拦截器、公共配置以及公共工具类等 - **acimage_feign**:放置各个模块的feign客户端 **前端** - **vue_manage_system**:后台管理页面 - **vue_acimage_web**:门户网站页面 **其它** - **doc**: 一些文档、图片和数据库文件 ## 运行与部署 **目前项目仍在不断完善,运行与部署流程以后再更新**。 - ~~将**doc/sql** 下的三个数据库分别导入到**mysql**中,四个数据库分别是四个前台服务对应的数据库~~ - ~~在每个服务的**application-dev.yml**文件中配置**mysql、redis、rabbitmq**、**nacos**相应的地址或账号密码~~ - ~~填写**acimage_common**中的**application-qiniu-template.yml**中的七牛云账号信息,包括**access-key**、**secret-key**、**domian**、**bucket**,或者给这四个属性随便赋值(不能为空,否则**NPE**),但是这样无法使用上传图片。并将**application-qiniu-template.yml**重命名为**application-qiniu.yml**~~ - ~~在**acimage_common**模块的下的**application.yml**配置 **nacos** 地址、**sentinel**地址(**sentinel**不配也不影响运行)~~ - ~~启动**nacos、redis、rabbitmq、mysql**~~ - ~~依次启动**acimage_user**、**acimage_community**、**acimage_image**、**acimage_gateway**,不这样启动的话可能会由于**rabbitmq**队列创建和绑定顺序的问题报错,如果遇到了,则全部服务再重新启动一遍。~~ - ~~运行前端(具体看**vue_acimage_web**的**README**)后点击默认弹出的链接即可访问首页~~ - ~~前台登录**:用户:wk,密码:wk123456 (还有几个用户可以从数据库sql文件看到,密码均为 用户名123456)~~ - 端口: - **acimage_user**: 8100 - **acimage_image** : 8090 - **acimage_community**: 8080 - **acimage_gateway**: 8070 ## 技术选型 #### 后端技术栈 **SpringBoot**、**SpringCloud**、**MyBatis-plus**、**Druid**(数据库连接池)、**Redis**(分布式缓存)、**Rabbitmq**(消息队列)、**Elasticsearch**(分布式搜索引擎)、**Minio**(对象存储服务)、**Nginx**(反向代理服务器)、**Docker**(应用容器引擎) #### 用户界面前端 **Vue2**、**Vue Router**(路由)、**ElementUi**(Vue基础组件库)、**axios**(http客户端)、**jsencrypt**(基于RSA加解密的js库)、**vue-dompurify-html**(防xss攻击)、**tinymce-vue**(富文本编辑器) #### 后台管理界面前端 **Vite**、**TypeSript** ## Todo - [x] 分类、标签 - [x] 话题、用户排行榜 - [x] 随机推荐 - [x] 相似话题推荐 - [x] 敏感词过滤 - [x] 接口幂等 - [x] 细化限流 - [x] 完善RABC权限控制,网关统一身份认证及鉴权 - [x] 增加邮件服务,邮箱验证,验证码等 - [x] 将图片存储在minio(原来存储在七牛云) - [x] elasticsearch多样化搜索,关键词高亮 - [x] 增加用户消息通知功能 - [ ] 利用redis+lua批量提取缓存中话题和浏览量等,减少通信次数,实时展示数据。 - [ ] 增加评论表情 - [ ] 增加关注功能 - [ ] 完善sentinel和nacos等配置和使用 - [ ] 增加爬虫模块 - [ ] 完善单元测试 - [ ] 等等等 ## 交流 项目起初是为了学习技术搭建的,由于能力有限,还有很多不完善的地方,欢迎各位能够指正。如果有人感兴趣(多么希望真的有人感兴趣 手动捂脸)或者该项目遇到什么问题或有什么建议提issue,可联系邮箱1179836161@qq.com或进群692992463交流。喜欢的话记得点个star。 ## 网站截图 **Web端** ![image text](./doc/images/home1.webp) ![image text](./doc/images/home2.webp) ![image text](./doc/images/forum1.webp) ![image text](./doc/images/forum2.webp) ![image text](./doc/images/forum3.webp) ![image text](./doc/images/topic-info1.webp) ![image text](./doc/images/topic-info2.webp) ![image text](./doc/images/search.webp) ![image text](./doc/images/search-image1.webp) ![image text](./doc/images/search-image2.webp) ![image text](./doc/images/login.webp) ![image text](./doc/images/activity.webp) ![image text](./doc/images/profile.webp) **Admin端** 管理系统随便放几张吧,反正管理系统都长一个样。 ![image text](./doc/images/admin-home.webp) ![image text](./doc/images/admin-authorize.webp) ## 开源协议 [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0.html) ================================================ FILE: acimage_admin/.gitignore ================================================ /target/ ================================================ FILE: acimage_admin/pom.xml ================================================ acimage com.acimage 0.0.1-SNAPSHOT 4.0.0 acimage_admin jar acimage_admin http://maven.apache.org UTF-8 com.acimage acimage_common 0.0.1-SNAPSHOT com.acimage acimage_feign 0.0.1-SNAPSHOT org.springframework.boot spring-boot-starter-aop com.alibaba druid mysql mysql-connector-java runtime com.baomidou dynamic-datasource-spring-boot-starter io.minio minio junit junit 3.8.1 test org.springframework.boot spring-boot-maven-plugin true org.apache.maven.plugins maven-surefire-plugin true ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/AdminApplication.java ================================================ package com.acimage.admin; import lombok.extern.slf4j.Slf4j; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.openfeign.EnableFeignClients; import org.springframework.context.annotation.ComponentScan; import org.springframework.scheduling.annotation.EnableScheduling; @Slf4j @SpringBootApplication @EnableDiscoveryClient @EnableScheduling @EnableFeignClients(basePackages="com.acimage.feign") @ComponentScan(value={"com.acimage"}) @MapperScan(basePackages = {"com.acimage.admin.dao"}) public class AdminApplication { public static void main(String[] args) { SpringApplication.run(AdminApplication.class, args); log.info("------------->>>Admin启动<<<-------------"); } } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/config/WebMvcConfig.java ================================================ package com.acimage.admin.config; import com.acimage.common.web.interceptor.AccessInterceptor; import com.acimage.common.web.interceptor.JwtInterceptor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Slf4j @Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new AccessInterceptor()).addPathPatterns("/**").order(30); } } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/config/mybatis/CommunityDataSourceConfig.java ================================================ package com.acimage.admin.config.mybatis; import com.baomidou.mybatisplus.core.config.GlobalConfig; import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean; import org.apache.ibatis.session.SqlSessionFactory; import org.mybatis.spring.SqlSessionFactoryBean; import org.mybatis.spring.SqlSessionTemplate; import org.mybatis.spring.annotation.MapperScan; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.jdbc.DataSourceBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import javax.sql.DataSource; @Deprecated //@Configuration //@MapperScan(basePackages = "com.acimage.admin.dao.community", sqlSessionFactoryRef = "communitySqlSessionFactory") public class CommunityDataSourceConfig { @Bean(name = "communityDataSource") @ConfigurationProperties(prefix = "spring.datasource.community") public DataSource communityDataSource() { return DataSourceBuilder.create().build(); } @Bean(name = "communitySqlSessionFactory") public SqlSessionFactory orderSqlSessionFactory(@Qualifier("communityDataSource")DataSource dataSource,@Autowired GlobalConfig globalConfig) throws Exception { MybatisSqlSessionFactoryBean sqlSessionFactoryBean=new MybatisSqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(dataSource); sqlSessionFactoryBean.setGlobalConfig(globalConfig); sqlSessionFactoryBean.setTypeAliasesPackage("com.acimage.common.model.domain"); sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/community/*.xml")); sqlSessionFactoryBean.setTypeHandlersPackage("com.acimage.admin.config.mybatis.typehandler"); return sqlSessionFactoryBean.getObject(); } @Bean(name = "communityTransactionManager") public DataSourceTransactionManager orderTransactionManager(@Qualifier("communityDataSource") DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } @Bean(name = "communitySqlSessionTemplate") public SqlSessionTemplate memberSqlSessionTemplate( @Qualifier("communitySqlSessionFactory") SqlSessionFactory sqlSessionFactory) throws Exception { return new SqlSessionTemplate(sqlSessionFactory); } } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/config/mybatis/GlobalConfigBean.java ================================================ package com.acimage.admin.config.mybatis; import com.baomidou.mybatisplus.core.MybatisConfiguration; import com.baomidou.mybatisplus.core.config.GlobalConfig; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; //@Configuration @Deprecated public class GlobalConfigBean { @Bean @ConfigurationProperties(prefix = "mybatis-plus.global-config") GlobalConfig globalConfig(){ return new GlobalConfig(); } } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/config/mybatis/ImageDataSourceConfig.java ================================================ package com.acimage.admin.config.mybatis; import com.alibaba.druid.pool.DruidDataSource; import com.baomidou.mybatisplus.core.MybatisConfiguration; import com.baomidou.mybatisplus.core.config.GlobalConfig; import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean; import org.apache.ibatis.session.SqlSessionFactory; import org.mybatis.spring.SqlSessionFactoryBean; import org.mybatis.spring.SqlSessionTemplate; import org.mybatis.spring.annotation.MapperScan; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.jdbc.DataSourceBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Lazy; import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import javax.sql.DataSource; @Deprecated //@Configuration //@MapperScan(basePackages = "com.acimage.admin.dao.image", sqlSessionFactoryRef = "imageSqlSessionFactory",sqlSessionTemplateRef ="imageSqlSessionTemplate") public class ImageDataSourceConfig { @Bean(name = "imageDataSource") @ConfigurationProperties(prefix = "spring.datasource.image") public DataSource imageDataSource() { return DataSourceBuilder.create().build(); } // @Bean // @ConfigurationProperties(prefix = "mybatis-plus.global-config") // GlobalConfig globalConfig(){ // return new GlobalConfig(); // } @Bean(name = "imageSqlSessionFactory") public SqlSessionFactory imageSqlSessionFactory(@Qualifier("imageDataSource") DataSource dataSource, @Autowired GlobalConfig globalConfig) throws Exception { MybatisSqlSessionFactoryBean sqlSessionFactoryBean=new MybatisSqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(dataSource); sqlSessionFactoryBean.setGlobalConfig(globalConfig); sqlSessionFactoryBean.setTypeAliasesPackage("com.acimage.common.model.domain"); sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/image/*.xml")); sqlSessionFactoryBean.setTypeHandlersPackage("com.acimage.admin.config.mybatis.typehandler"); return sqlSessionFactoryBean.getObject(); } @Bean(name = "imageTransactionManager") public DataSourceTransactionManager imageTransactionManager(@Qualifier("imageDataSource") DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } @Bean(name = "imageSqlSessionTemplate") public SqlSessionTemplate memberSqlSessionTemplate( @Qualifier("imageSqlSessionFactory") SqlSessionFactory sqlSessionFactory) throws Exception { return new SqlSessionTemplate(sqlSessionFactory); } } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/dao/community/CategoryDao.java ================================================ package com.acimage.admin.dao.community; import com.acimage.common.model.domain.community.Category; import com.baomidou.mybatisplus.core.mapper.BaseMapper; public interface CategoryDao extends BaseMapper { } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/dao/community/CommentDao.java ================================================ package com.acimage.admin.dao.community; import com.acimage.common.model.domain.community.Comment; import com.baomidou.mybatisplus.core.mapper.BaseMapper; public interface CommentDao extends BaseMapper { } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/dao/community/HomeCarouselDao.java ================================================ package com.acimage.admin.dao.community; import com.acimage.common.model.domain.community.HomeCarousel; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Select; import java.util.List; public interface HomeCarouselDao extends BaseMapper { @Select("select coalesce(max(location),0) from tb_home_carousel") Integer getMaxLocation(); List count(); } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/dao/community/TopicDao.java ================================================ package com.acimage.admin.dao.community; import com.acimage.common.model.domain.community.Topic; import com.baomidou.mybatisplus.core.mapper.BaseMapper; public interface TopicDao extends BaseMapper { } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/dao/sys/ApiDao.java ================================================ package com.acimage.admin.dao.sys; import com.acimage.common.model.domain.sys.Api; import com.baomidou.mybatisplus.core.mapper.BaseMapper; public interface ApiDao extends BaseMapper { } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/dao/sys/AuthorizeDao.java ================================================ package com.acimage.admin.dao.sys; import com.acimage.common.model.domain.sys.Authorize; import com.baomidou.mybatisplus.core.mapper.BaseMapper; public interface AuthorizeDao extends BaseMapper { } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/dao/sys/PermissionDao.java ================================================ package com.acimage.admin.dao.sys; import com.acimage.common.model.domain.sys.Permission; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Param; import javax.annotation.Nullable; import java.util.List; public interface PermissionDao extends BaseMapper { List selectTreeByParentId(@Nullable @Param("parentId") Integer parentId); List selectPermissionsWithParent(@Param("startIndex") int startIndex,@Param("recordNumber") int recordNumber); } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/dao/sys/RoleDao.java ================================================ package com.acimage.admin.dao.sys; import com.acimage.common.model.domain.sys.Role; import com.baomidou.mybatisplus.core.mapper.BaseMapper; public interface RoleDao extends BaseMapper { } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/dao/sys/UserRoleDao.java ================================================ package com.acimage.admin.dao.sys; import com.acimage.common.model.domain.sys.UserRole; import com.baomidou.mybatisplus.core.mapper.BaseMapper; public interface UserRoleDao extends BaseMapper { } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/dao/user/UserDao.java ================================================ package com.acimage.admin.dao.user; import com.acimage.common.model.domain.user.User; import com.baomidou.mybatisplus.core.mapper.BaseMapper; public interface UserDao extends BaseMapper { } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/dao/user/UserPrivacyDao.java ================================================ package com.acimage.admin.dao.user; import com.acimage.common.model.domain.user.UserPrivacy; import com.baomidou.mybatisplus.core.mapper.BaseMapper; public interface UserPrivacyDao extends BaseMapper { } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/global/consts/ModuleConstants.java ================================================ package com.acimage.admin.global.consts; public class ModuleConstants { public static final String COMMUNITY="community"; public static final String SYS="sys"; public static final String IMAGE="image"; public static final String USER="user"; } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/model/request/AdminLoginReq.java ================================================ package com.acimage.admin.model.request; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import javax.validation.constraints.Email; import javax.validation.constraints.Size; @Data @AllArgsConstructor @NoArgsConstructor public class AdminLoginReq { @Email(message = "邮箱格式错误") @Size(min=6,max=32,message = "邮箱长度在6到32之间") private String email; @Size(min = 100, max = 2000, message = "非法密码") String password; @Size(min=4,max=6,message = "验证码长度不符") String verifyCode; } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/model/request/ApiAddReq.java ================================================ package com.acimage.admin.model.request; import com.acimage.common.global.enums.MatchRule; import com.acimage.common.global.enums.MyHttpMethod; import com.acimage.common.model.domain.sys.Api; import com.acimage.common.model.domain.sys.Permission; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import org.springframework.http.HttpMethod; import javax.annotation.Nullable; import javax.validation.constraints.*; @Data @NoArgsConstructor @AllArgsConstructor public class ApiAddReq { public static final int PATH_MAX = 200; public static final int PATH_MIN = 2; public static final int NOTE_MAX = 100; @NotBlank @Pattern(regexp = Api.PATH_PATTERN) @Size(min= Api.PATH_MIN,max=Api.PATH_MAX) String path; @NotNull MyHttpMethod method; @Positive @NotNull Integer permissionId; @Size(max=Api.NOTE_MAX) String note; boolean enable; } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/model/request/ApiModifyReq.java ================================================ package com.acimage.admin.model.request; import com.acimage.common.global.enums.MatchRule; import com.acimage.common.global.enums.MyHttpMethod; import com.acimage.common.model.domain.sys.Api; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import org.springframework.http.HttpMethod; import javax.validation.constraints.NotNull; import javax.validation.constraints.Pattern; import javax.validation.constraints.Positive; import javax.validation.constraints.Size; @Data @NoArgsConstructor @AllArgsConstructor public class ApiModifyReq { public static final int PATH_MAX = 200; public static final int PATH_MIN = 2; public static final int NOTE_MAX = 100; @Positive Integer id; @Size(min = Api.PATH_MIN, max = Api.PATH_MAX) @Pattern(regexp = Api.PATH_PATTERN) String path; @NotNull MyHttpMethod method; @Positive @NotNull Integer permissionId; @Size(max = Api.NOTE_MAX) String note; boolean enable; } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/model/request/ApiQueryReq.java ================================================ package com.acimage.admin.model.request; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import javax.validation.constraints.Max; import javax.validation.constraints.Min; @Data @NoArgsConstructor @AllArgsConstructor public class ApiQueryReq { String keyword; @Min(1) Integer pageNo; @Min(2) @Max(20) Integer pageSize; } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/model/request/CarouselAddReq.java ================================================ package com.acimage.admin.model.request; import com.acimage.common.model.domain.community.HomeCarousel; import lombok.Data; import lombok.NoArgsConstructor; import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; @Data @NoArgsConstructor public class CarouselAddReq { @Size(min = HomeCarousel.DESC_MIN, max = HomeCarousel.DESC_MAX, message = HomeCarousel.DESC_INVALID_MSG) String description; @NotNull @Size(min = 0, max = HomeCarousel.LINK_MAX, message = HomeCarousel.LINK_INVALID_MSG) String link; } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/model/request/CarouselModifyReq.java ================================================ package com.acimage.admin.model.request; import com.acimage.common.model.domain.community.HomeCarousel; import lombok.Data; import lombok.NoArgsConstructor; import javax.validation.constraints.NotNull; import javax.validation.constraints.Positive; import javax.validation.constraints.Size; @Data @NoArgsConstructor public class CarouselModifyReq { @NotNull @Positive private Integer id; @Size(min = HomeCarousel.DESC_MIN, max = HomeCarousel.DESC_MAX, message = HomeCarousel.DESC_INVALID_MSG) String description; @NotNull @Size(min = 0, max = HomeCarousel.LINK_MAX, message = HomeCarousel.LINK_INVALID_MSG) String link; } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/model/request/CommentQueryReq.java ================================================ package com.acimage.admin.model.request; import lombok.Data; import lombok.NoArgsConstructor; import org.hibernate.validator.constraints.Range; import javax.validation.constraints.*; @Data @NoArgsConstructor public class CommentQueryReq { Long topicId; String keyword; @Range(min=1) Integer pageNo; @Range(min=5,max=20) Integer pageSize; } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/model/request/PermissionAddReq.java ================================================ package com.acimage.admin.model.request; import com.acimage.common.model.domain.sys.Permission; import lombok.Data; import lombok.NoArgsConstructor; import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; @Data @NoArgsConstructor public class PermissionAddReq { Integer parentId; @Size(max = Permission.CODE_MAX,message =Permission.CODE_VALIDATION_MSG) String code; @Size(max = Permission.NOTE_MAX,message =Permission.NOTE_VALIDATION_MSG) String note; @NotNull @Size(max = Permission.LABEL_MAX,message =Permission.LABEL_VALIDATION_MSG) String label; @NotNull Boolean module; } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/model/request/PermissionModifyReq.java ================================================ package com.acimage.admin.model.request; import com.acimage.common.model.domain.sys.Permission; import lombok.Data; import lombok.NoArgsConstructor; import javax.validation.constraints.NotNull; import javax.validation.constraints.Positive; import javax.validation.constraints.Size; @Data @NoArgsConstructor public class PermissionModifyReq { @Positive Integer id; Integer parentId; @Size(max = Permission.CODE_MAX,message =Permission.CODE_VALIDATION_MSG) String code; @Size(max = Permission.NOTE_MAX,message =Permission.NOTE_VALIDATION_MSG) String note; @NotNull @Size(max = Permission.LABEL_MAX,message =Permission.LABEL_VALIDATION_MSG) String label; } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/model/request/RoleAddReq.java ================================================ package com.acimage.admin.model.request; import com.acimage.common.model.domain.sys.Role; import lombok.Data; import lombok.NoArgsConstructor; import javax.validation.constraints.Size; @Data @NoArgsConstructor public class RoleAddReq { @Size(max= Role.ROLE_NAME_MAX,min=Role.ROLE_NAME_MIN,message = Role.ROLE_NAME_VALIDATION_MSG) String roleName; @Size(max=Role.NOTE_MAX,message = Role.NOTE_VALIDATION_MSG) String note; } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/model/request/RoleModifyReq.java ================================================ package com.acimage.admin.model.request; import com.acimage.common.model.domain.sys.Role; import lombok.Data; import lombok.NoArgsConstructor; import javax.validation.constraints.Positive; import javax.validation.constraints.Size; @Data @NoArgsConstructor public class RoleModifyReq { @Positive Integer id; @Size(max= Role.ROLE_NAME_MAX,min=Role.ROLE_NAME_MIN,message = Role.ROLE_NAME_VALIDATION_MSG) String roleName; @Size(max=Role.NOTE_MAX,message = Role.NOTE_VALIDATION_MSG) String note; } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/model/request/TopicQueryReq.java ================================================ package com.acimage.admin.model.request; import lombok.Data; import lombok.NoArgsConstructor; import org.hibernate.validator.constraints.Range; import javax.validation.constraints.*; @Data @NoArgsConstructor public class TopicQueryReq { @NotBlank String column; @Positive @NotNull Integer pageNo; @Range(min=5,max=20) Integer pageSize; } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/model/request/UserQueryReq.java ================================================ package com.acimage.admin.model.request; import lombok.Data; import javax.validation.constraints.*; @Data public class UserQueryReq { String keyword; @NotNull @Positive Integer pageNo; @Min(2) @Max(20) Integer pageSize; } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/model/vo/WebsiteDataVo.java ================================================ package com.acimage.admin.model.vo; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor public class WebsiteDataVo { private Integer pageView; private Integer apiAccessCount; } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/service/api/ApiQueryService.java ================================================ package com.acimage.admin.service.api; import com.acimage.admin.model.request.ApiQueryReq; import com.acimage.common.model.domain.sys.Api; import com.acimage.common.model.page.MyPage; public interface ApiQueryService { MyPage pageBy(ApiQueryReq apiQueryReq); } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/service/api/ApiWriteService.java ================================================ package com.acimage.admin.service.api; import com.acimage.admin.model.request.ApiAddReq; import com.acimage.admin.model.request.ApiModifyReq; public interface ApiWriteService { void save(ApiAddReq apiAddReq); void update(ApiModifyReq apiModifyReq); void delete(int id); } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/service/api/impl/ApiQueryServiceImpl.java ================================================ package com.acimage.admin.service.api.impl; import com.acimage.admin.dao.sys.ApiDao; import com.acimage.admin.global.consts.ModuleConstants; import com.acimage.admin.model.request.ApiQueryReq; import com.acimage.admin.service.api.ApiQueryService; import com.acimage.common.model.domain.sys.Api; import com.acimage.common.model.page.MyPage; import com.baomidou.dynamic.datasource.annotation.DS; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service @DS(ModuleConstants.SYS) public class ApiQueryServiceImpl implements ApiQueryService { @Autowired ApiDao apiDao; @Override public MyPage pageBy(ApiQueryReq apiQueryReq) { IPage page = new Page<>(apiQueryReq.getPageNo(), apiQueryReq.getPageSize()); LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); qw.orderByAsc(Api::getPath); String keyword = apiQueryReq.getKeyword(); if (keyword != null) { qw.like(Api::getPath, keyword); } IPage resultPage=apiDao.selectPage(page, qw); return MyPage.from(resultPage); } } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/service/api/impl/ApiWriteServiceImpl.java ================================================ package com.acimage.admin.service.api.impl; import com.acimage.admin.dao.sys.ApiDao; import com.acimage.admin.global.consts.ModuleConstants; import com.acimage.admin.model.request.ApiAddReq; import com.acimage.admin.model.request.ApiModifyReq; import com.acimage.admin.service.api.ApiWriteService; import com.acimage.admin.service.permission.PermissionQueryService; import com.acimage.common.global.exception.BusinessException; import com.acimage.common.model.domain.sys.Api; import com.acimage.common.model.domain.sys.Permission; import com.acimage.common.utils.common.BeanUtils; import com.acimage.common.utils.common.ListUtils; import com.baomidou.dynamic.datasource.annotation.DS; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.Date; import java.util.List; @Service @DS(ModuleConstants.SYS) public class ApiWriteServiceImpl implements ApiWriteService { @Autowired ApiDao apiDao; @Autowired PermissionQueryService permissionQueryService; @Override public void save(ApiAddReq apiAddReq){ List permissionList=permissionQueryService.listByModule(false); List permissionIds= ListUtils.extract(Permission::getId,permissionList); if(!permissionIds.contains(apiAddReq.getPermissionId())){ throw new BusinessException("权限不存在或非可用权限"); } Api api= BeanUtils.copyPropertiesTo(apiAddReq,Api.class); api.setCreateTime(new Date()); api.setUpdateTime(new Date()); apiDao.insert(api); } @Override public void update(ApiModifyReq apiModifyReq){ List permissionList=permissionQueryService.listByModule(false); List permissionIds= ListUtils.extract(Permission::getId,permissionList); if(!permissionIds.contains(apiModifyReq.getPermissionId())){ throw new BusinessException("权限不存在或非可用权限"); } Api api= BeanUtils.copyPropertiesTo(apiModifyReq,Api.class); api.setCreateTime(new Date()); api.setUpdateTime(new Date()); apiDao.updateById(api); } @Override public void delete(int id){ apiDao.deleteById(id); } } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/service/authorize/AuthorizeQueryService.java ================================================ package com.acimage.admin.service.authorize; import com.acimage.common.model.domain.sys.Authorize; import java.util.List; public interface AuthorizeQueryService { List listAuthorizeByRoleId(int roleId); } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/service/authorize/AuthorizeWriteService.java ================================================ package com.acimage.admin.service.authorize; public interface AuthorizeWriteService { void save(int roleId, int permissionId); void remove(int roleId, int permissionId); void remove(int permissionId); } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/service/authorize/impl/AuthorizeQueryServiceImpl.java ================================================ package com.acimage.admin.service.authorize.impl; import com.acimage.admin.dao.sys.AuthorizeDao; import com.acimage.admin.service.authorize.AuthorizeQueryService; import com.acimage.common.model.domain.sys.Authorize; import com.baomidou.dynamic.datasource.annotation.DS; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; @Service @DS("sys") public class AuthorizeQueryServiceImpl implements AuthorizeQueryService { @Autowired AuthorizeDao authorizeDao; @Override public List listAuthorizeByRoleId(int roleId){ LambdaQueryWrapper qw=new LambdaQueryWrapper<>(); qw.eq(Authorize::getRoleId,roleId); return authorizeDao.selectList(qw); } } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/service/authorize/impl/AuthorizeWriteServiceImpl.java ================================================ package com.acimage.admin.service.authorize.impl; import com.acimage.admin.dao.sys.AuthorizeDao; import com.acimage.admin.service.authorize.AuthorizeWriteService; import com.acimage.admin.service.permission.PermissionQueryService; import com.acimage.common.global.exception.BusinessException; import com.acimage.common.model.domain.sys.Authorize; import com.acimage.common.model.domain.sys.Permission; import com.baomidou.dynamic.datasource.annotation.DS; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service @DS("sys") public class AuthorizeWriteServiceImpl implements AuthorizeWriteService { @Autowired AuthorizeDao authorizeDao; @Autowired PermissionQueryService permissionQueryService; @Override public void save(int roleId, int permissionId) { Permission permission = permissionQueryService.getPermission(permissionId); if (permission.isModule()) { throw new BusinessException("该结点为模块,不可授权"); } Authorize authorize = new Authorize(roleId, permissionId); authorizeDao.insert(authorize); } @Override public void remove(int roleId, int permissionId) { LambdaQueryWrapper uw = new LambdaQueryWrapper<>(); uw.eq(Authorize::getRoleId, roleId) .eq(Authorize::getPermissionId, permissionId); authorizeDao.delete(uw); } @Override public void remove(int permissionId) { LambdaQueryWrapper uw = new LambdaQueryWrapper<>(); uw.eq(Authorize::getPermissionId, permissionId); authorizeDao.delete(uw); } } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/service/category/CategoryQueryService.java ================================================ package com.acimage.admin.service.category; import com.acimage.common.model.domain.community.Category; import java.util.List; public interface CategoryQueryService { List listAll(); } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/service/category/impl/CategoryQueryServiceImpl.java ================================================ package com.acimage.admin.service.category.impl; import com.acimage.admin.dao.community.CategoryDao; import com.acimage.admin.global.consts.ModuleConstants; import com.acimage.admin.service.category.CategoryQueryService; import com.acimage.common.model.domain.community.Category; import com.baomidou.dynamic.datasource.annotation.DS; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; @DS(ModuleConstants.COMMUNITY) @Service public class CategoryQueryServiceImpl implements CategoryQueryService { @Autowired CategoryDao categoryDao; @Override public List listAll(){ return categoryDao.selectList(null); } } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/service/comment/CommentQueryService.java ================================================ package com.acimage.admin.service.comment; import com.acimage.admin.model.request.CommentQueryReq; import com.acimage.common.model.domain.community.Comment; import com.acimage.common.model.page.MyPage; public interface CommentQueryService { MyPage pageCommentsBy(CommentQueryReq commentQueryReq); } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/service/comment/CommentWriteService.java ================================================ package com.acimage.admin.service.comment; public interface CommentWriteService { void delete(long id); } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/service/comment/impl/CommentQueryServiceImpl.java ================================================ package com.acimage.admin.service.comment.impl; import cn.hutool.core.util.StrUtil; import com.acimage.admin.dao.community.CommentDao; import com.acimage.admin.global.consts.ModuleConstants; import com.acimage.admin.model.request.CommentQueryReq; import com.acimage.admin.service.comment.CommentQueryService; import com.acimage.common.model.domain.community.Comment; import com.acimage.common.model.page.MyPage; import com.baomidou.dynamic.datasource.annotation.DS; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service @DS(ModuleConstants.COMMUNITY) public class CommentQueryServiceImpl implements CommentQueryService { @Autowired CommentDao commentDao; @Override public MyPage pageCommentsBy(CommentQueryReq commentQueryReq) { String keyword=commentQueryReq.getKeyword(); Long topicId=commentQueryReq.getTopicId(); int pageNo = commentQueryReq.getPageNo(); int pageSize = commentQueryReq.getPageSize(); IPage page=new Page<>(pageNo,pageSize); LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); qw.orderByDesc(Comment::getCreateTime); if(!StrUtil.isBlank(keyword)){ qw.like(Comment::getContent,keyword); } if(topicId!=null&&topicId>0){ qw.eq(Comment::getTopicId,topicId); } IPage resultPage = commentDao.selectPage(page,qw); return MyPage.from(resultPage); } } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/service/comment/impl/CommentWriteServiceImpl.java ================================================ package com.acimage.admin.service.comment.impl; import com.acimage.admin.global.consts.ModuleConstants; import com.acimage.admin.service.comment.CommentWriteService; import com.acimage.feign.client.CommentClient; import com.baomidou.dynamic.datasource.annotation.DS; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service @DS(ModuleConstants.COMMUNITY) public class CommentWriteServiceImpl implements CommentWriteService { @Autowired private CommentClient client; @Override public void delete(long id){ client.delete(id); } } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/service/homecarousel/HomeCarouselQueryService.java ================================================ package com.acimage.admin.service.homecarousel; import com.acimage.common.model.domain.community.HomeCarousel; import java.util.List; public interface HomeCarouselQueryService { List listCurrent(); } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/service/homecarousel/HomeCarouselWriteService.java ================================================ package com.acimage.admin.service.homecarousel; import com.acimage.admin.model.request.CarouselAddReq; import com.acimage.admin.model.request.CarouselModifyReq; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; public interface HomeCarouselWriteService { @Transactional(rollbackFor = Exception.class) void saveHomeCarouselImage(MultipartFile multipartFile, CarouselAddReq carouselAddReq); void deleteHomeCarouselImage(long id); void updateHomeCarouselImage(CarouselModifyReq modifyReq); @Transactional(rollbackFor = Exception.class) void coverHomeCarouselImage(long id, MultipartFile multipartFile); } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/service/homecarousel/impl/HomeCarouselQueryServiceImpl.java ================================================ package com.acimage.admin.service.homecarousel.impl; import com.acimage.admin.dao.community.HomeCarouselDao; import com.acimage.admin.global.consts.ModuleConstants; import com.acimage.admin.service.homecarousel.HomeCarouselQueryService; import com.acimage.common.model.domain.community.HomeCarousel; import com.baomidou.dynamic.datasource.annotation.DS; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; @Service @DS(ModuleConstants.COMMUNITY) public class HomeCarouselQueryServiceImpl implements HomeCarouselQueryService { @Autowired HomeCarouselDao homeCarouselDao; @Override public List listCurrent() { LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); qw.orderByAsc(HomeCarousel::getLocation); return homeCarouselDao.selectList(qw); } } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/service/homecarousel/impl/HomeCarouselWriteServiceImpl.java ================================================ package com.acimage.admin.service.homecarousel.impl; import cn.hutool.core.util.RandomUtil; import com.acimage.admin.dao.community.HomeCarouselDao; import com.acimage.admin.global.consts.ModuleConstants; import com.acimage.admin.model.request.CarouselAddReq; import com.acimage.admin.model.request.CarouselModifyReq; import com.acimage.admin.service.homecarousel.HomeCarouselWriteService; import com.acimage.common.global.consts.StorePrefixConstants; import com.acimage.common.global.context.UserContext; import com.acimage.common.global.exception.BusinessException; import com.acimage.common.model.domain.community.HomeCarousel; import com.acimage.common.utils.common.FileUtils; import com.acimage.common.utils.IdGenerator; import com.acimage.common.utils.minio.MinioUtils; import com.baomidou.dynamic.datasource.annotation.DS; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; import java.util.Date; @Slf4j @Service @DS(ModuleConstants.COMMUNITY) public class HomeCarouselWriteServiceImpl implements HomeCarouselWriteService { @Autowired HomeCarouselDao homeCarouselDao; @Autowired MinioUtils minioUtils; @Override public void saveHomeCarouselImage(MultipartFile multipartFile, CarouselAddReq carouselAddReq) { String description = carouselAddReq.getDescription(); String link = carouselAddReq.getLink().trim(); int length = 5; String idString = RandomUtil.randomString(length); int size = (int) multipartFile.getSize(); int location = homeCarouselDao.getMaxLocation() + 1; Date now = new Date(); String fileName = String.format("%s.%s", idString, FileUtils.formatOf(multipartFile)); String url = minioUtils.generateBaseUrl(StorePrefixConstants.HOME_CAROUSEL, now, fileName); String newUrl = minioUtils.upload(multipartFile, url); HomeCarousel homeCarousel = new HomeCarousel(); homeCarousel.setSize(size); homeCarousel.setDescription(description); homeCarousel.setCreateTime(now); homeCarousel.setUpdateTime(now); homeCarousel.setUrl(newUrl); homeCarousel.setLocation(location); homeCarousel.setLink(link); homeCarouselDao.insert(homeCarousel); } @Override public void deleteHomeCarouselImage(long id) { HomeCarousel homeCarousel = homeCarouselDao.selectById(id); if (homeCarousel == null) { log.error("用户:{} 操作:删除主页走马灯 特殊图片id:{} 错误:图片不存在", UserContext.getUsername(), id); throw new BusinessException("该图片不存在"); } LambdaUpdateWrapper qw = new LambdaUpdateWrapper<>(); qw.eq(HomeCarousel::getId, id); int col = homeCarouselDao.delete(qw); minioUtils.deleteFile(homeCarousel.getUrl()); } @Override public void updateHomeCarouselImage(CarouselModifyReq modifyReq) { Integer id = modifyReq.getId(); String description = modifyReq.getDescription(); String link = modifyReq.getLink(); LambdaUpdateWrapper uw = new LambdaUpdateWrapper<>(); uw.set(HomeCarousel::getDescription, description) .set(HomeCarousel::getLink, link) .set(HomeCarousel::getUpdateTime, new Date()) .eq(HomeCarousel::getId, id); int col = homeCarouselDao.update(null, uw); if (col == 0) { log.error("用户:{} 操作:修改主页走马灯描述 特殊图片id:{} 错误:图片非主页走马灯图片", UserContext.getUsername(), id); throw new BusinessException("该主页走马灯图片不存在"); } } @Override public void coverHomeCarouselImage(long id, MultipartFile multipartFile) { HomeCarousel homeCarousel = homeCarouselDao.selectById(id); if (homeCarouselDao == null) { log.error("用户:{} 操作:覆盖主页走马灯 特殊图片id:{} 错误:图片不存在", UserContext.getUsername(), id); throw new BusinessException("图片不存在"); } int size = (int) multipartFile.getSize(); Date now = new Date(); String oldUrl = homeCarousel.getUrl(); String suffix = String.format("%s.%s", IdGenerator.getSnowflakeNextId(), FileUtils.formatOf(multipartFile)); String newUrl = minioUtils.generateBaseUrl(StorePrefixConstants.HOME_CAROUSEL, now, suffix); newUrl = minioUtils.upload(multipartFile, newUrl); LambdaUpdateWrapper uw = new LambdaUpdateWrapper<>(); uw.set(HomeCarousel::getSize, size) .set(HomeCarousel::getUpdateTime, now) .set(HomeCarousel::getUrl, newUrl) .eq(HomeCarousel::getId, id); homeCarouselDao.update(null, uw); //删除文件 minioUtils.deleteFile(oldUrl); } } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/service/login/LoginService.java ================================================ package com.acimage.admin.service.login; import com.acimage.admin.model.request.AdminLoginReq; public interface LoginService { String login(AdminLoginReq adminLoginReq); } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/service/login/VerifyCodeService.java ================================================ package com.acimage.admin.service.login; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; public interface VerifyCodeService { void writeCodeImageToResponseAndRecord(HttpServletRequest request, HttpServletResponse response); boolean verifyAndRemoveIfSuccess(HttpServletRequest request, String code); } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/service/login/impl/LoginServiceImpl.java ================================================ package com.acimage.admin.service.login.impl; import cn.hutool.crypto.digest.DigestUtil; import com.acimage.admin.dao.user.UserDao; import com.acimage.admin.dao.user.UserPrivacyDao; import com.acimage.admin.global.consts.ModuleConstants; import com.acimage.admin.model.request.AdminLoginReq; import com.acimage.admin.service.login.LoginService; import com.acimage.common.global.consts.JwtConstants; import com.acimage.common.global.exception.BusinessException; import com.acimage.common.model.domain.user.User; import com.acimage.common.model.domain.user.UserPrivacy; import com.acimage.common.service.TokenService; import com.acimage.common.utils.RsaUtils; import com.baomidou.dynamic.datasource.annotation.DS; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Slf4j @Service @DS(ModuleConstants.USER) public class LoginServiceImpl implements LoginService { @Autowired UserPrivacyDao userPrivacyDao; @Autowired UserDao userDao; @Autowired TokenService tokenService; @Override public String login(AdminLoginReq adminLoginReq) { String email = adminLoginReq.getEmail(); String password = adminLoginReq.getPassword(); LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); qw.eq(UserPrivacy::getEmail, email); UserPrivacy userPrivacy = userPrivacyDao.selectOne(qw); if (userPrivacy == null) { throw new BusinessException("邮箱不存在"); } //找到密码 long userId=userPrivacy.getUserId(); String salt = userPrivacy.getSalt(); String passwordDigest = userPrivacy.getPwd(); //获取私钥 String privateKey = RsaUtils.getPrivateKey(); //解密密码 String passwordDecrypt = RsaUtils.decrypt(privateKey, password); // log.debug(" decrypt as:{}", passwordDecrypt); //判断密码正确性 if (!DigestUtil.md5Hex(salt + passwordDecrypt).equals(passwordDigest)) { log.warn("登录 错误:邮箱{} 或密码错误", email); throw new BusinessException("用户名或密码错误"); } //返回token User user=userDao.selectById(userId); return tokenService.createAndRecordToken(userId, user.getUsername(), user.getPhotoUrl(), JwtConstants.ADMIN_EXPIRE_DAYS); } } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/service/login/impl/VerifyCodeServiceImpl.java ================================================ package com.acimage.admin.service.login.impl; import cn.hutool.captcha.CaptchaUtil; import cn.hutool.captcha.ShearCaptcha; import com.acimage.admin.service.login.VerifyCodeService; import com.acimage.common.utils.redis.RedisUtils; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.concurrent.TimeUnit; @Slf4j @Service public class VerifyCodeServiceImpl implements VerifyCodeService { @Autowired RedisUtils redisUtils; public static final String STRINGKP_VERIFY_CODE="acimage:admin:verifyCode:sessionId:"; @Override public void writeCodeImageToResponseAndRecord(HttpServletRequest request, HttpServletResponse response){ int width=100; int height=40; int codeCount=4; int thickness=4; ShearCaptcha captcha = CaptchaUtil.createShearCaptcha(width, height, codeCount, thickness); //图形验证码写出,可以写出到文件,也可以写出到流 try { captcha.write(response.getOutputStream()); } catch (IOException e) { log.error("response.getOutputStream()错误 {}",e.getMessage()); throw new RuntimeException(e); } //获取验证码中的文字内容 String verifyCode = captcha.getCode(); //记录到redis String sessionId=request.getSession().getId(); long timeout=30L; redisUtils.setAsString(STRINGKP_VERIFY_CODE+sessionId,verifyCode,timeout, TimeUnit.SECONDS); } @Override public boolean verifyAndRemoveIfSuccess(HttpServletRequest request, String code){ String key=STRINGKP_VERIFY_CODE+request.getSession().getId(); String trueCode=redisUtils.getForString(key); if(trueCode==null){ return false; } if(trueCode.equals(code)){ redisUtils.delete(key); return true; }else{ return false; } } } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/service/permission/PermissionQueryService.java ================================================ package com.acimage.admin.service.permission; import com.acimage.common.model.domain.sys.Permission; import com.acimage.common.model.page.MyPage; import java.util.List; public interface PermissionQueryService { Permission getPermission(int id); List getPermissionTree(); MyPage pagePermissionsWithParent(int pageNo, int pageSize); List listByModule(boolean isModule); } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/service/permission/PermissionWriteSercice.java ================================================ package com.acimage.admin.service.permission; import com.acimage.admin.model.request.PermissionAddReq; import com.acimage.admin.model.request.PermissionModifyReq; import org.springframework.transaction.annotation.Transactional; public interface PermissionWriteSercice { void save(PermissionAddReq permissionAddReq); @Transactional void remove(int id); @Transactional void update(PermissionModifyReq permissionModifyReq); } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/service/permission/impl/PermissionQueryServiceImpl.java ================================================ package com.acimage.admin.service.permission.impl; import com.acimage.admin.dao.sys.PermissionDao; import com.acimage.admin.service.permission.PermissionQueryService; import com.acimage.common.model.domain.sys.Permission; import com.acimage.common.model.page.MyPage; import com.acimage.common.utils.common.PageUtils; import com.baomidou.dynamic.datasource.annotation.DS; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; @Service @DS("sys") public class PermissionQueryServiceImpl implements PermissionQueryService { @Autowired PermissionDao permissionDao; @Override public Permission getPermission(int id){ return permissionDao.selectById(id); } @Override public List getPermissionTree(){ return permissionDao.selectTreeByParentId(null); } @Override public MyPage pagePermissionsWithParent(int pageNo, int pageSize){ int startIndex= PageUtils.startIndexOf(pageNo,pageSize); List permissionList= permissionDao.selectPermissionsWithParent(startIndex,pageSize); int count=permissionDao.selectCount(null); return new MyPage<>(count,permissionList); } @Override public List listByModule(boolean isModule){ LambdaQueryWrapper qw=new LambdaQueryWrapper<>(); qw.eq(Permission::isModule,isModule).orderByDesc(Permission::getCode); return permissionDao.selectList(qw); } } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/service/permission/impl/PermissionWriteServiceImpl.java ================================================ package com.acimage.admin.service.permission.impl; import cn.hutool.core.bean.BeanUtil; import com.acimage.admin.dao.sys.PermissionDao; import com.acimage.admin.global.consts.ModuleConstants; import com.acimage.admin.model.request.PermissionAddReq; import com.acimage.admin.model.request.PermissionModifyReq; import com.acimage.admin.service.authorize.AuthorizeWriteService; import com.acimage.admin.service.permission.PermissionWriteSercice; import com.acimage.common.model.domain.sys.Permission; import com.baomidou.dynamic.datasource.annotation.DS; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.Date; @Service @DS(ModuleConstants.SYS) public class PermissionWriteServiceImpl implements PermissionWriteSercice { @Autowired PermissionDao permissionDao; @Autowired AuthorizeWriteService authorizeWriteService; @Override public void save(PermissionAddReq permissionAddReq){ Permission permission=new Permission(); BeanUtil.copyProperties(permissionAddReq,permission); permissionDao.insert(permission); } @Override public void remove(int id){ authorizeWriteService.remove(id); permissionDao.deleteById(id); } @Override public void update(PermissionModifyReq permissionModifyReq){ Permission permission=new Permission(); BeanUtil.copyProperties(permissionModifyReq,permission); permission.setUpdateTime(new Date()); permissionDao.updateById(permission); } } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/service/role/RoleQueryService.java ================================================ package com.acimage.admin.service.role; import com.acimage.common.model.domain.sys.Role; import java.util.List; public interface RoleQueryService { List listAll(); } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/service/role/RoleWriteService.java ================================================ package com.acimage.admin.service.role; import com.acimage.admin.model.request.RoleAddReq; import com.acimage.admin.model.request.RoleModifyReq; public interface RoleWriteService { void save(RoleAddReq roleAddReq); void remove(long id); void update(RoleModifyReq roleModifyReq); } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/service/role/impl/RoleQueryServiceImpl.java ================================================ package com.acimage.admin.service.role.impl; import com.acimage.admin.dao.sys.RoleDao; import com.acimage.admin.service.role.RoleQueryService; import com.acimage.common.model.domain.sys.Role; import com.baomidou.dynamic.datasource.annotation.DS; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; @Service @DS("sys") public class RoleQueryServiceImpl implements RoleQueryService { @Autowired RoleDao roleDao; @Override public List listAll(){ return roleDao.selectList(null); } } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/service/role/impl/RoleWriteServiceImpl.java ================================================ package com.acimage.admin.service.role.impl; import cn.hutool.core.bean.BeanUtil; import com.acimage.admin.dao.sys.RoleDao; import com.acimage.admin.model.request.RoleAddReq; import com.acimage.admin.model.request.RoleModifyReq; import com.acimage.admin.service.role.RoleWriteService; import com.acimage.common.model.domain.sys.Role; import com.baomidou.dynamic.datasource.annotation.DS; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.Date; @Service @DS("sys") public class RoleWriteServiceImpl implements RoleWriteService { @Autowired RoleDao roleDao; @Override public void save(RoleAddReq roleAddReq) { Role role = new Role(); BeanUtil.copyProperties(roleAddReq,role,false); roleDao.insert(role); } @Override public void remove(long id) { roleDao.deleteById(id); } @Override public void update(RoleModifyReq roleModifyReq) { LambdaUpdateWrapper uw = new LambdaUpdateWrapper<>(); uw.eq(Role::getId, roleModifyReq.getId()) .set(Role::getRoleName, roleModifyReq.getRoleName()) .set(Role::getNote, roleModifyReq.getNote()) .set(Role::getUpdateTime, new Date()); roleDao.update(null, uw); } } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/service/topic/TopicQueryService.java ================================================ package com.acimage.admin.service.topic; import com.acimage.admin.model.request.TopicQueryReq; import com.acimage.common.model.domain.community.Topic; import com.acimage.common.model.page.MyPage; public interface TopicQueryService { MyPage listOrderByColumn(TopicQueryReq topicQueryReq); Integer getTopicCount(); } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/service/topic/TopicWriteService.java ================================================ package com.acimage.admin.service.topic; public interface TopicWriteService { void remove(long topicId); } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/service/topic/impl/TopicQueryServiceImpl.java ================================================ package com.acimage.admin.service.topic.impl; import com.acimage.admin.dao.community.TopicDao; import com.acimage.admin.global.consts.ModuleConstants; import com.acimage.admin.model.request.TopicQueryReq; import com.acimage.admin.service.topic.TopicQueryService; import com.acimage.common.model.domain.community.Topic; import com.acimage.common.model.page.MyPage; import com.acimage.common.utils.common.PageUtils; import com.acimage.common.utils.redis.RedisUtils; import com.baomidou.dynamic.datasource.annotation.DS; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.toolkit.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; import java.util.concurrent.TimeUnit; @DS(ModuleConstants.COMMUNITY) @Service public class TopicQueryServiceImpl implements TopicQueryService { public static final String STRINGKP_TOPIC_COUNT = "acimage:admin:topic:totalCount:"; @Autowired TopicDao topicDao; @Autowired RedisUtils redisUtils; @Override public MyPage listOrderByColumn(TopicQueryReq topicQueryReq) { int pageNo = topicQueryReq.getPageNo(); int pageSize = topicQueryReq.getPageSize(); String column = StringUtils.camelToUnderline(topicQueryReq.getColumn()) ; int startIndex = PageUtils.startIndexOf(pageNo, pageSize); QueryWrapper qw = new QueryWrapper<>(); qw.orderByDesc(column).last(String.format("limit %d,%d", startIndex, pageSize)); List topicList = topicDao.selectList(qw); int count = this.getTopicCount(); return new MyPage<>(count, topicList); } @Override public Integer getTopicCount() { Integer totalCount = redisUtils.getForString(STRINGKP_TOPIC_COUNT, Integer.class); if (totalCount != null) { return totalCount; } LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); qw.select(Topic::getId); totalCount = topicDao.selectCount(qw); long timeout = 2L; redisUtils.setAsString(STRINGKP_TOPIC_COUNT, totalCount.toString(), timeout, TimeUnit.MINUTES); return totalCount; } } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/service/topic/impl/TopicWriteServiceImpl.java ================================================ package com.acimage.admin.service.topic.impl; import com.acimage.admin.global.consts.ModuleConstants; import com.acimage.admin.service.topic.TopicWriteService; import com.acimage.common.global.exception.BusinessException; import com.acimage.common.result.Result; import com.acimage.feign.client.TopicClient; import com.baomidou.dynamic.datasource.annotation.DS; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Slf4j @Service @DS(ModuleConstants.COMMUNITY) public class TopicWriteServiceImpl implements TopicWriteService { @Autowired TopicClient topicClient; @Override public void remove(long topicId){ Result result=topicClient.delete(topicId); if(!result.isOk()){ log.error("话题删除失败 id:{}",topicId); throw new BusinessException("话题删除失败"); } } } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/service/user/UserQueryService.java ================================================ package com.acimage.admin.service.user; import com.acimage.admin.model.request.UserQueryReq; import com.acimage.common.model.domain.user.User; import com.acimage.common.model.page.MyPage; public interface UserQueryService { MyPage listBy(UserQueryReq userQueryReq); User getUser(long userId); } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/service/user/UserWriteService.java ================================================ package com.acimage.admin.service.user; public interface UserWriteService { } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/service/user/impl/UserQueryServiceImpl.java ================================================ package com.acimage.admin.service.user.impl; import com.acimage.admin.dao.user.UserDao; import com.acimage.admin.global.consts.ModuleConstants; import com.acimage.admin.model.request.UserQueryReq; import com.acimage.admin.service.user.UserQueryService; import com.acimage.admin.service.userrole.UserRoleQueryService; import com.acimage.common.model.domain.user.User; import com.acimage.common.model.page.MyPage; import com.acimage.common.utils.common.ListUtils; import com.baomidou.dynamic.datasource.annotation.DS; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.List; import java.util.Map; @Service @DS(ModuleConstants.USER) public class UserQueryServiceImpl implements UserQueryService { @Autowired UserDao userDao; @Autowired UserRoleQueryService userRoleQueryService; @Override public MyPage listBy(UserQueryReq userQueryReq) { IPage page = new Page<>(userQueryReq.getPageNo(), userQueryReq.getPageSize()); String like = userQueryReq.getKeyword(); IPage resultPage; if (like != null && like.trim().length() > 0) { LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); qw.like(User::getUsername, userQueryReq.getKeyword()); page = userDao.selectPage(page, qw); resultPage = userDao.selectPage(page, qw); } else { resultPage = userDao.selectPage(page, null); } List userList = resultPage.getRecords(); int totalCount = (int) resultPage.getTotal(); Map> map = userRoleQueryService.listUserIdWithRoleIds(ListUtils.extract(User::getId, userList)); for (User user : userList) { List roleIds = map.get(user.getId()); if (roleIds == null) { user.setRoleIds(new ArrayList<>()); } else { user.setRoleIds(roleIds); } } return new MyPage<>(totalCount, userList); } @Override public User getUser(long userId){ return userDao.selectById(userId); } } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/service/user/impl/UserWriteServiceImpl.java ================================================ package com.acimage.admin.service.user.impl; import com.acimage.admin.global.consts.ModuleConstants; import com.baomidou.dynamic.datasource.annotation.DS; import org.springframework.stereotype.Service; @Service @DS(ModuleConstants.USER) public class UserWriteServiceImpl { } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/service/userrole/UserRoleQueryService.java ================================================ package com.acimage.admin.service.userrole; import com.acimage.common.model.domain.sys.UserRole; import java.util.List; import java.util.Map; public interface UserRoleQueryService { Map> listUserIdWithRoleIds(List userIds); UserRole getUserRole(long userId, int roleId); } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/service/userrole/UserRoleWriteService.java ================================================ package com.acimage.admin.service.userrole; import java.util.List; import java.util.Map; public interface UserRoleWriteService { void save(long userId, int roleId); void remove(long userId, int roleId); } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/service/userrole/impl/UserRoleQueryServiceImpl.java ================================================ package com.acimage.admin.service.userrole.impl; import com.acimage.admin.dao.sys.UserRoleDao; import com.acimage.admin.global.consts.ModuleConstants; import com.acimage.admin.service.userrole.UserRoleQueryService; import com.acimage.common.model.domain.sys.UserRole; import com.baomidou.dynamic.datasource.annotation.DS; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @Service @DS(ModuleConstants.SYS) public class UserRoleQueryServiceImpl implements UserRoleQueryService { @Autowired UserRoleDao userRoleDao; @Override public Map> listUserIdWithRoleIds(List userIds) { LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); qw.in(UserRole::getUserId, userIds); List userRoles = userRoleDao.selectList(qw); //组装 Map> map = new HashMap<>(); for (UserRole userRole : userRoles) { long userId = userRole.getUserId(); List roleIds = map.get(userId); if (roleIds == null) { roleIds = new ArrayList<>(); roleIds.add(userRole.getRoleId()); map.put(userId,roleIds); }else{ roleIds.add(userRole.getRoleId()); } } return map; } @Override public UserRole getUserRole(long userId, int roleId){ LambdaQueryWrapper qw=new LambdaQueryWrapper<>(); qw.eq(UserRole::getUserId,userId) .eq(UserRole::getRoleId,roleId); return userRoleDao.selectOne(qw); } } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/service/userrole/impl/UserRoleWriteServiceImpl.java ================================================ package com.acimage.admin.service.userrole.impl; import com.acimage.admin.dao.sys.UserRoleDao; import com.acimage.admin.global.consts.ModuleConstants; import com.acimage.admin.service.user.UserQueryService; import com.acimage.admin.service.userrole.UserRoleQueryService; import com.acimage.admin.service.userrole.UserRoleWriteService; import com.acimage.common.global.exception.BusinessException; import com.acimage.common.model.domain.sys.UserRole; import com.acimage.common.utils.IdGenerator; import com.baomidou.dynamic.datasource.annotation.DS; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.Date; @DS(ModuleConstants.SYS) @Service public class UserRoleWriteServiceImpl implements UserRoleWriteService { @Autowired UserRoleDao userRoleDao; @Autowired UserRoleQueryService userRoleQueryService; @Autowired UserQueryService userQueryService; @Override public void save(long userId, int roleId) { if (userRoleQueryService.getUserRole(userId, roleId) != null) { throw new BusinessException("该用户已有该角色"); } if (userQueryService.getUser(userId) == null) { throw new BusinessException("该用户不存在"); } long id = IdGenerator.getSnowflakeNextId(); UserRole userRole = new UserRole(id, userId, roleId, new Date()); userRoleDao.insert(userRole); } @Override public void remove(long userId, int roleId) { LambdaQueryWrapper qw=new LambdaQueryWrapper<>(); qw.eq(UserRole::getUserId,userId) .eq(UserRole::getRoleId,roleId); userRoleDao.delete(qw); } } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/service/websitedata/WebsiteDataService.java ================================================ package com.acimage.admin.service.websitedata; import com.acimage.admin.model.vo.WebsiteDataVo; public interface WebsiteDataService { WebsiteDataVo getWebsiteData(); } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/service/websitedata/impl/WebsiteDataServiceImpl.java ================================================ package com.acimage.admin.service.websitedata.impl; import com.acimage.admin.model.vo.WebsiteDataVo; import com.acimage.admin.service.websitedata.WebsiteDataService; import com.acimage.common.global.consts.SysKeyConstants; import com.acimage.common.utils.redis.RedisUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class WebsiteDataServiceImpl implements WebsiteDataService { @Autowired RedisUtils redisUtils; @Override public WebsiteDataVo getWebsiteData() { WebsiteDataVo websiteDataVo = new WebsiteDataVo(); Long pageViewLong = redisUtils.sizeForHyperLogLog(SysKeyConstants.LOGK_PAGE_VIEW); Integer apiAccessCount = redisUtils.getForString(SysKeyConstants.STRINGK_INTERFACE_TOTAL, Integer.class); if (pageViewLong != null) { websiteDataVo.setPageView(pageViewLong.intValue()); } websiteDataVo.setApiAccessCount(apiAccessCount); return websiteDataVo; } } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/web/controller/AdminLoginController.java ================================================ package com.acimage.admin.web.controller; import com.acimage.admin.model.request.AdminLoginReq; import com.acimage.admin.service.login.LoginService; import com.acimage.admin.service.login.VerifyCodeService; import com.acimage.common.redis.annotation.RequestLimit; import com.acimage.common.redis.enums.LimitTarget; import com.acimage.common.result.Result; import com.acimage.common.utils.RsaUtils; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @RestController @Slf4j @RequestMapping("/api/admin/logins") @Validated public class AdminLoginController { @Autowired LoginService loginService; @Autowired VerifyCodeService verifyCodeService; @RequestLimit(limitTimes = {1}, durations = {2}, penaltyTimes = {-1}, targets = {LimitTarget.IP}) @PostMapping("/doLogin") public Result doLogin(@Validated @RequestBody AdminLoginReq adminLoginReq, HttpServletRequest request) { String code = adminLoginReq.getVerifyCode(); boolean verifyCorrect = verifyCodeService.verifyAndRemoveIfSuccess(request, code); if (!verifyCorrect) { return Result.fail("验证码错误,请重新验证"); } return Result.ok(loginService.login(adminLoginReq)); } @GetMapping("/publicKey") public Result getPublicKey() { return Result.ok(RsaUtils.getPublicKey()); } @GetMapping("/commonCode") public Result getCommonVerifyCode(HttpServletRequest request, HttpServletResponse response) { verifyCodeService.writeCodeImageToResponseAndRecord(request,response); return Result.ok(); } } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/web/controller/ApiOperateController.java ================================================ package com.acimage.admin.web.controller; import com.acimage.admin.model.request.ApiAddReq; import com.acimage.admin.model.request.ApiModifyReq; import com.acimage.admin.service.api.ApiWriteService; import com.acimage.common.result.Result; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import javax.validation.constraints.NotNull; import javax.validation.constraints.Positive; @RestController @Slf4j @RequestMapping("/api/admin/apis/operate") @Validated public class ApiOperateController { @Autowired ApiWriteService apiWriteService; @PostMapping public Result addApi(@Validated @RequestBody ApiAddReq apiAddreq){ String path=apiAddreq.getPath().trim(); if(path.length()<2){ return Result.fail("路径有效长度不少于2"); } apiAddreq.setPath(path); apiWriteService.save(apiAddreq); return Result.ok(); } @PutMapping public Result modifyApi(@Validated @RequestBody ApiModifyReq apiModifyReq){ String path=apiModifyReq.getPath().trim(); if(path.length()<2){ return Result.fail("路径有效长度不少于2"); } apiModifyReq.setPath(path); apiWriteService.update(apiModifyReq); return Result.ok(); } @DeleteMapping("/{id}") public Result deleteApi(@Positive @NotNull @PathVariable int id){ apiWriteService.delete(id); return Result.ok(); } } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/web/controller/ApiQueryController.java ================================================ package com.acimage.admin.web.controller; import com.acimage.admin.model.request.ApiQueryReq; import com.acimage.admin.service.api.ApiQueryService; import com.acimage.common.model.domain.sys.Api; import com.acimage.common.model.page.MyPage; import com.acimage.common.result.Result; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; @RestController @Slf4j @RequestMapping("/api/admin/apis/query") @Validated public class ApiQueryController { @Autowired ApiQueryService apiQueryService; @GetMapping("/search") public Result> queryApisBy(@Validated @ModelAttribute ApiQueryReq apiQueryReq){ return Result.ok(apiQueryService.pageBy(apiQueryReq)); } } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/web/controller/AuthorizeOperateController.java ================================================ package com.acimage.admin.web.controller; import com.acimage.admin.service.authorize.AuthorizeWriteService; import com.acimage.common.result.Result; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import javax.validation.constraints.Positive; @RestController @Slf4j @RequestMapping("/api/admin/authorizes") @Validated public class AuthorizeOperateController { @Autowired AuthorizeWriteService authorizeWriteService; @PostMapping public Result addAuthorize(@RequestParam @Positive Integer roleId, @RequestParam @Positive Integer permissionId){ authorizeWriteService.save(roleId,permissionId); return Result.ok(); } @DeleteMapping("/{roleId}/{permissionId}") public Result deleteAuthorize(@PathVariable @Positive Integer roleId, @PathVariable @Positive Integer permissionId){ authorizeWriteService.remove(roleId,permissionId); return Result.ok(); } } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/web/controller/AuthorizeQueryController.java ================================================ package com.acimage.admin.web.controller; import com.acimage.admin.service.authorize.AuthorizeQueryService; import com.acimage.common.model.domain.sys.Authorize; import com.acimage.common.result.Result; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import javax.validation.constraints.Positive; import java.util.List; @RestController @Slf4j @RequestMapping("/api/admin/authorizes") @Validated public class AuthorizeQueryController { @Autowired AuthorizeQueryService authorizeQueryService; @GetMapping("/roleId/{roleId}") public Result> queryRoleAuthorize(@PathVariable @Positive Integer roleId){ return Result.ok(authorizeQueryService.listAuthorizeByRoleId(roleId)); } } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/web/controller/CategoryController.java ================================================ package com.acimage.admin.web.controller; import com.acimage.admin.service.category.CategoryQueryService; import com.acimage.common.model.domain.community.Category; import com.acimage.common.result.Result; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.List; @RestController @Slf4j @RequestMapping("/api/admin/categories") @Validated public class CategoryController { @Autowired CategoryQueryService categoryQueryService; @GetMapping("/all") public Result> queryAll(){ return Result.ok(categoryQueryService.listAll()) ; } } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/web/controller/CommentOperateController.java ================================================ package com.acimage.admin.web.controller; import com.acimage.admin.model.request.CommentQueryReq; import com.acimage.admin.service.comment.CommentQueryService; import com.acimage.admin.service.comment.CommentWriteService; import com.acimage.common.model.domain.community.Comment; import com.acimage.common.model.page.MyPage; import com.acimage.common.result.Result; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import javax.validation.constraints.Positive; @RestController @Slf4j @RequestMapping("/api/admin/comments/operate") @Validated public class CommentOperateController { @Autowired CommentWriteService commentWriteService; @DeleteMapping("/{id}") public Result pageCommentsBy(@PathVariable @Positive Long id) { commentWriteService.delete(id); return Result.ok(); } } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/web/controller/CommentQueryController.java ================================================ package com.acimage.admin.web.controller; import com.acimage.admin.model.request.CommentQueryReq; import com.acimage.admin.model.request.TopicQueryReq; import com.acimage.admin.service.comment.CommentQueryService; import com.acimage.admin.service.topic.TopicQueryService; import com.acimage.common.model.domain.community.Comment; import com.acimage.common.model.domain.community.Topic; import com.acimage.common.model.page.MyPage; import com.acimage.common.result.Result; import com.acimage.common.utils.LambdaUtils; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.List; @RestController @Slf4j @RequestMapping("/api/admin/comments/query") @Validated public class CommentQueryController { @Autowired CommentQueryService commentQueryService; @GetMapping("/by") public Result> pageCommentsBy(@Validated @ModelAttribute CommentQueryReq queryReq) { return Result.ok(commentQueryService.pageCommentsBy(queryReq)); } } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/web/controller/HomeCarouselController.java ================================================ package com.acimage.admin.web.controller; import cn.hutool.core.util.StrUtil; import com.acimage.admin.model.request.CarouselAddReq; import com.acimage.admin.model.request.CarouselModifyReq; import com.acimage.admin.service.homecarousel.HomeCarouselWriteService; import com.acimage.admin.service.homecarousel.HomeCarouselQueryService; import com.acimage.common.global.consts.FileFormatConstants; import com.acimage.common.model.domain.community.HomeCarousel; import com.acimage.common.result.Result; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import javax.validation.constraints.Positive; import javax.validation.constraints.Size; import java.util.List; @RestController @Slf4j @RequestMapping("/api/admin/homeCarousels") @Validated public class HomeCarouselController { @Autowired HomeCarouselWriteService homeCarouselWriteService; @Autowired HomeCarouselQueryService homeCarouselQueryService; @PostMapping public Result addImage(@RequestParam("image") MultipartFile imageFile, @Validated @ModelAttribute CarouselAddReq carouselAddReq) { String originName = imageFile.getOriginalFilename(); String format = StrUtil.subAfter(originName, '.', true); if (!FileFormatConstants.ALLOWED_CAROUSEL_FORMAT.contains(format)) { return Result.fail("图片格式需为"+FileFormatConstants.ALLOWED_CAROUSEL_FORMAT); } homeCarouselWriteService.saveHomeCarouselImage(imageFile, carouselAddReq); return Result.ok(); } @DeleteMapping("/{id}") public Result deleteImage(@PathVariable @Positive Long id) { homeCarouselWriteService.deleteHomeCarouselImage(id); return Result.ok(); } @PutMapping("/descriptionAndLink") public Result modifyDescription(@Validated @ModelAttribute CarouselModifyReq carouselModifyReq) { homeCarouselWriteService.updateHomeCarouselImage(carouselModifyReq); return Result.ok(); } @PostMapping("/cover") public Result coverImageFile(@RequestParam("id") @Positive Long id, @RequestParam("image") MultipartFile imageFile) { String originName = imageFile.getOriginalFilename(); String format = StrUtil.subAfter(originName, '.', true); if (!FileFormatConstants.ALLOWED_CAROUSEL_FORMAT.contains(format)) { return Result.fail("图片格式需为"+FileFormatConstants.ALLOWED_CAROUSEL_FORMAT); } homeCarouselWriteService.coverHomeCarouselImage(id, imageFile); return Result.ok(); } @GetMapping("/current") public Result> queryCurrentHomeCarousel() { return Result.ok(homeCarouselQueryService.listCurrent()); } } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/web/controller/PermissionOperateController.java ================================================ package com.acimage.admin.web.controller; import cn.hutool.core.util.StrUtil; import com.acimage.admin.model.request.PermissionAddReq; import com.acimage.admin.model.request.PermissionModifyReq; import com.acimage.admin.service.permission.PermissionWriteSercice; import com.acimage.common.result.Result; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import javax.validation.constraints.Positive; @RestController @Slf4j @Validated @RequestMapping("/api/admin/permissions") public class PermissionOperateController { @Autowired PermissionWriteSercice permissionWriteSercice; @PostMapping public Result add(@Validated @RequestBody PermissionAddReq permissionAddReq){ if(permissionAddReq.getCode()==null|| StrUtil.isBlank(permissionAddReq.getCode())){ permissionAddReq.setCode(null); } if(!permissionAddReq.getModule() &&permissionAddReq.getCode()==null){ return Result.fail("非模块的权限码不能为空"); } permissionWriteSercice.save(permissionAddReq); return Result.ok(); } @PutMapping public Result modify(@Validated @RequestBody PermissionModifyReq permissionModifyReq){ permissionWriteSercice.update(permissionModifyReq); return Result.ok(); } @DeleteMapping("/{id}") public Result modify(@PathVariable @Positive Integer id){ permissionWriteSercice.remove(id); return Result.ok(); } } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/web/controller/PermissionQueryController.java ================================================ package com.acimage.admin.web.controller; import com.acimage.admin.service.permission.PermissionQueryService; import com.acimage.common.model.domain.sys.Permission; import com.acimage.common.model.page.MyPage; import com.acimage.common.result.Result; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.validation.constraints.Max; import javax.validation.constraints.Positive; import java.util.List; @RestController @Slf4j @Validated @RequestMapping("/api/admin/permissions") public class PermissionQueryController { @Autowired PermissionQueryService permissionQueryService; @GetMapping("/tree") public Result> queryPermissionTree(){ return Result.ok(permissionQueryService.getPermissionTree()); } @GetMapping("/page/{pageNo}/{pageSize}") public Result> pagePermissionsWithParent(@PathVariable @Positive Integer pageNo, @PathVariable @Max(20) Integer pageSize){ return Result.ok(permissionQueryService.pagePermissionsWithParent(pageNo,pageSize)); } @GetMapping("/modules") public Result> queryModules(){ return Result.ok(permissionQueryService.listByModule(true)); } @GetMapping("/nonModules") public Result> queryNonModules(){ return Result.ok(permissionQueryService.listByModule(false)); } } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/web/controller/RoleController.java ================================================ package com.acimage.admin.web.controller; import com.acimage.admin.model.request.RoleAddReq; import com.acimage.admin.model.request.RoleModifyReq; import com.acimage.admin.service.role.RoleQueryService; import com.acimage.admin.service.role.RoleWriteService; import com.acimage.common.model.domain.sys.Role; import com.acimage.common.result.Result; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import javax.validation.constraints.Positive; import java.util.List; @RestController @Slf4j @RequestMapping("/api/admin/roles") @Validated public class RoleController { @Autowired RoleQueryService roleQueryService; @Autowired RoleWriteService roleWriteService; @GetMapping("/all") public Result> queryAllRoles(){ return Result.ok(roleQueryService.listAll()); } @PostMapping() public Result add(@Validated @RequestBody RoleAddReq roleAddReq){ roleWriteService.save(roleAddReq); return Result.ok(); } @DeleteMapping("/{id}") public Result delete(@PathVariable @Positive Integer id){ roleWriteService.remove(id); return Result.ok(); } @PutMapping() public Result modify(@Validated @RequestBody RoleModifyReq roleModifyReq){ roleWriteService.update(roleModifyReq); return Result.ok(); } } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/web/controller/TopicOperateController.java ================================================ package com.acimage.admin.web.controller; import com.acimage.admin.service.topic.TopicWriteService; import com.acimage.common.result.Result; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.validation.constraints.Positive; @RestController @Slf4j @RequestMapping("/api/admin/topics/operate") @Validated public class TopicOperateController { @Autowired TopicWriteService topicWriteService; @DeleteMapping("/{topicId}") public Result delete(@PathVariable @Positive Long topicId){ topicWriteService.remove(topicId); return Result.ok(); } } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/web/controller/TopicQueryController.java ================================================ package com.acimage.admin.web.controller; import com.acimage.admin.model.request.TopicQueryReq; import com.acimage.admin.service.topic.TopicQueryService; import com.acimage.common.model.domain.community.Topic; import com.acimage.common.result.Result; import com.acimage.common.utils.LambdaUtils; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import java.util.List; @RestController @Slf4j @RequestMapping("/api/admin/topics/query") @Validated public class TopicQueryController { @Autowired TopicQueryService topicQueryService; @GetMapping("/orderBy") public Result listOrderBy(@Validated @ModelAttribute TopicQueryReq topicQueryReq) { List allowedColumns= LambdaUtils.columnsFrom(Topic::getUpdateTime,Topic::getCreateTime,Topic::getPageView,Topic::getStarCount,Topic::getCommentCount); if(!allowedColumns.contains(topicQueryReq.getColumn())){ return Result.fail("非法查询字段"); } return Result.ok(topicQueryService.listOrderByColumn(topicQueryReq)); } } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/web/controller/UserQueryController.java ================================================ package com.acimage.admin.web.controller; import com.acimage.admin.model.request.UserQueryReq; import com.acimage.admin.service.user.UserQueryService; import com.acimage.common.model.domain.user.User; import com.acimage.common.model.page.MyPage; import com.acimage.common.result.Result; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @Slf4j @RequestMapping("/api/admin/users/query") @Validated public class UserQueryController { @Autowired UserQueryService userQueryService; @GetMapping("/search") public Result> queryUsersBy(@Validated @ModelAttribute UserQueryReq userQueryReq){ String keyword=userQueryReq.getKeyword(); if(keyword!=null){ if(keyword.trim().length()==0){ userQueryReq.setKeyword(null); }else{ userQueryReq.setKeyword(keyword.trim()); } } return Result.ok(userQueryService.listBy(userQueryReq)); } } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/web/controller/UserRoleOperateController.java ================================================ package com.acimage.admin.web.controller; import com.acimage.admin.model.request.UserQueryReq; import com.acimage.admin.service.userrole.UserRoleWriteService; import com.acimage.common.model.domain.user.User; import com.acimage.common.model.page.MyPage; import com.acimage.common.result.Result; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import javax.validation.constraints.NotNull; import javax.validation.constraints.Positive; @RestController @Slf4j @RequestMapping("/api/admin/userRoles/operate") @Validated public class UserRoleOperateController { @Autowired UserRoleWriteService userRoleWriteService; @PostMapping public Result addRoleForUser(@Positive @NotNull Long userId, @Positive @NotNull Integer roleId) { userRoleWriteService.save(userId, roleId); return Result.ok(); } @DeleteMapping("/{userId}/{roleId}") public Result deleteRoleForUser(@PathVariable @Positive @NotNull Long userId,@PathVariable @Positive @NotNull Integer roleId) { userRoleWriteService.remove(userId, roleId); return Result.ok(); } } ================================================ FILE: acimage_admin/src/main/java/com/acimage/admin/web/controller/WebsiteDataController.java ================================================ package com.acimage.admin.web.controller; import com.acimage.admin.model.vo.WebsiteDataVo; import com.acimage.admin.service.websitedata.WebsiteDataService; import com.acimage.common.result.Result; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @Slf4j @RequestMapping("/api/admin/websites") @Validated public class WebsiteDataController { @Autowired WebsiteDataService websiteDataService; @GetMapping("/accessData") public Result queryWebsiteData() { return Result.ok(websiteDataService.getWebsiteData()); } } ================================================ FILE: acimage_admin/src/main/resources/application-dev.yml ================================================ server: port: 8060 spring: config: activate: on-profile: - dev # datasource: # community: # type: com.alibaba.druid.pool.DruidDataSource # driver-class-name: com.mysql.cj.jdbc.Driver # jdbc-url: jdbc:mysql://localhost:3306/acimage_community?useSSl=false&allowMultiQueries=true&serverTimezone=UTC # username: root # password: mysql # image: # type: com.alibaba.druid.pool.DruidDataSource # driver-class-name: com.mysql.cj.jdbc.Driver # jdbc-url: jdbc:mysql://localhost:3306/acimage_image?useSSl=false&allowMultiQueries=true&serverTimezone=UTC # username: root # password: mysql datasource: dynamic: primary: sys #设置默认的数据源或者数据源组,默认值即为master strict: true #严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源 datasource: sys: url: jdbc:mysql://localhost:3306/acimage_sys?useSSl=false&allowMultiQueries=true&serverTimezone=UTC username: root password: mysql driver-class-name: com.mysql.cj.jdbc.Driver # 3.2.0开始支持SPI可省略此配置 community: url: jdbc:mysql://localhost:3306/acimage_community?useSSl=false&allowMultiQueries=true&serverTimezone=UTC username: root password: mysql driver-class-name: com.mysql.cj.jdbc.Driver # 3.2.0开始支持SPI可省略此配置 image: url: jdbc:mysql://localhost:3306/acimage_image?useSSl=false&allowMultiQueries=true&serverTimezone=UTC username: root password: mysql driver-class-name: com.mysql.cj.jdbc.Driver # 3.2.0开始支持SPI可省略此配置 user: url: jdbc:mysql://localhost:3306/acimage_user?useSSl=false&allowMultiQueries=true&serverTimezone=UTC username: root password: mysql driver-class-name: com.mysql.cj.jdbc.Driver # 3.2.0开始支持SPI可省略此配置 redis: host: 127.0.0.1 port: 6379 password: redis lettuce: pool: max-active: 8 max-idle: 8 #最大空闲连接 min-idle: 0 #最小空闲连接 max-wait: 100ms #连接等待时间 cloud: nacos: server-addr: 43.136.68.91:8848 #nacos 服务地址 ================================================ FILE: acimage_admin/src/main/resources/application.yml ================================================ spring: profiles: include: common,common-secret active: dev2 application: name: admin-service #服务名称 servlet: multipart: max-file-size: 10MB max-request-size: 55MB # rabbitmq: # host: 192.168.130.128 # port: 5672 # username: acimage # password: acimage # virtual-host: /acimage # listener: # simple: # auto-startup: false #消费者是否启动 # direct: # auto-startup: false #生产者是否启动 mybatis-plus: mapper-locations: classpath*:mapper/**/*.xml type-aliases-package: com.acimage.common.model.domain type-handlers-package: com.acimage.common.config.typehandler global-config: db-config: table-prefix: tb_ feign: okhttp: enabled: true httpclient: max-connections: 20 # 最大的连接数 max-connections-per-route: 5 # 每个路径的最大连接数 ================================================ FILE: acimage_admin/src/main/resources/logback-spring.xml ================================================ %d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%20.20thread{20}] %40logger{40} : %msg%n INFO ACCEPT DENY ${LOG_HOME}/${spring.application.name}/%d{yyyy-MM-dd}-info.%i.log 5MB 8 %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n WARN ACCEPT DENY ${LOG_HOME}/${spring.application.name}/%d{yyyy-MM-dd}-warn.%i.log 5MB 15 %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n ERROR ACCEPT DENY ${LOG_HOME}/${spring.application.name}/%d{yyyy-MM-dd}-error.%i.log 5MB 15 %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n ================================================ FILE: acimage_admin/src/main/resources/mapper/community/TopicMapper.xml ================================================ ================================================ FILE: acimage_admin/src/main/resources/mapper/image/HomeCarouselMapper.xml ================================================ ================================================ FILE: acimage_admin/src/main/resources/mapper/sys/ApiMapper.xml ================================================ ================================================ FILE: acimage_admin/src/main/resources/mapper/sys/PermissionMapper.xml ================================================ ================================================ FILE: acimage_admin/src/main/resources/mapper/user/TopicMapper.xml ================================================ ================================================ FILE: acimage_admin/src/test/java/com/acimage/admin/AdminApplicationTest.java ================================================ package com.acimage.admin; import com.acimage.common.deprecated.QiniuUtils; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import java.io.File; @SpringBootTest class AdminApplicationTest { @Autowired QiniuUtils qiniuUtils; @Test void qiniuUtilsTest() { File file = new File("F:\\MyImage\\素材\\bg2.png"); qiniuUtils.upload(file,"test/xlg.png"); } } ================================================ FILE: acimage_admin/src/test/java/com/acimage/admin/dao/community/SpDaoTest.java ================================================ package com.acimage.admin.dao.community; import com.acimage.common.model.domain.community.HomeCarousel; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import java.util.Date; @SpringBootTest public class SpDaoTest { @Autowired HomeCarouselDao homeCarouselDao; @Test public void deleteTest(){ long id=10086L; homeCarouselDao.deleteById(id); } @Test public void getMaxLocationOfHomeCarousel(){ System.out.println(homeCarouselDao.getMaxLocation()); } @Test public void testSelectAll(){ System.out.println(homeCarouselDao.selectList(null)); System.out.println(homeCarouselDao.count()); } } ================================================ FILE: acimage_admin/src/test/java/com/acimage/admin/dao/community/TopicDaoTest.java ================================================ package com.acimage.admin.dao.community; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest public class TopicDaoTest { @Autowired TopicDao topicDao; @Test void testSelectAll(){ System.out.println(topicDao.selectList(null)); } } ================================================ FILE: acimage_admin/src/test/java/com/acimage/admin/dao/sys/ApiDaoTest.java ================================================ package com.acimage.admin.dao.sys; import com.acimage.common.global.enums.MatchRule; import com.acimage.common.model.domain.sys.Api; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.HttpMethod; @SpringBootTest public class ApiDaoTest { @Autowired ApiDao apiDao; @Test void testSelect(){ System.out.println(apiDao.selectList(null)); } } ================================================ FILE: acimage_admin/src/test/java/com/acimage/admin/service/HomeCarouselWriteServiceTest.java ================================================ package com.acimage.admin.service; import com.acimage.admin.service.homecarousel.HomeCarouselWriteService; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest public class HomeCarouselWriteServiceTest { @Autowired HomeCarouselWriteService homeCarouselWriteService; } ================================================ FILE: acimage_common/.gitignore ================================================ /.idea /src/main/resources/application-qiniu.yml ================================================ FILE: acimage_common/pom.xml ================================================ acimage com.acimage 0.0.1-SNAPSHOT 4.0.0 acimage_common jar acimage_common http://maven.apache.org UTF-8 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-aop com.baomidou mybatis-plus-boot-starter cn.hutool hutool-all org.springframework.boot spring-boot-starter-data-redis org.apache.commons commons-pool2 org.springframework.boot spring-boot-starter-amqp true com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery org.springframework.cloud spring-cloud-starter-netflix-ribbon com.auth0 java-jwt com.qiniu qiniu-java-sdk true com.squareup.okhttp3 okhttp 3.14.2 compile true io.minio minio true io.github.toolgood toolgood-words true org.springframework.data spring-data-elasticsearch true org.springframework.boot spring-boot-starter-validation net.coobird thumbnailator junit junit 3.8.1 test org.springframework.boot spring-boot-maven-plugin true exec true org.apache.maven.plugins maven-surefire-plugin true ================================================ FILE: acimage_common/src/main/java/com/acimage/common/CommonMain.java ================================================ package com.acimage.common; public class CommonMain { public static void main(String[] args) { } } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/config/EsConfig.java ================================================ package com.acimage.common.config; import org.apache.http.auth.AuthScope; import org.apache.http.auth.UsernamePasswordCredentials; import org.apache.http.client.CredentialsProvider; import org.apache.http.impl.client.BasicCredentialsProvider; import org.elasticsearch.client.RestClientBuilder; import org.elasticsearch.client.RestHighLevelClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; import java.util.concurrent.TimeUnit; @Profile(value = {"test","dev","dev2","server"}) @Configuration @ConditionalOnClass(RestHighLevelClient.class) public class EsConfig { @Bean public RestHighLevelClient devRestHighLevelClient(@Autowired RestClientBuilder restClientBuilder) { return new RestHighLevelClient(restClientBuilder.setHttpClientConfigCallback(requestConfig -> { final CredentialsProvider credentialsProvider = new BasicCredentialsProvider(); return requestConfig.setDefaultCredentialsProvider(credentialsProvider); })); } } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/config/EsProdConfig.java ================================================ package com.acimage.common.config; import org.apache.http.auth.AuthScope; import org.apache.http.auth.UsernamePasswordCredentials; import org.apache.http.client.CredentialsProvider; import org.apache.http.impl.client.BasicCredentialsProvider; import org.elasticsearch.client.RestClientBuilder; import org.elasticsearch.client.RestHighLevelClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; import java.util.concurrent.TimeUnit; @Profile("prod") @Configuration @ConditionalOnClass(RestHighLevelClient.class) public class EsProdConfig { @Value("${spring.elasticsearch.username}") private String username; @Value("${spring.elasticsearch.password}") private String password; @Bean public RestHighLevelClient prodRestHighLevelClient(@Autowired RestClientBuilder restClientBuilder) { return new RestHighLevelClient(restClientBuilder.setHttpClientConfigCallback(requestConfig -> { final CredentialsProvider credentialsProvider = new BasicCredentialsProvider(); credentialsProvider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(username, password)); requestConfig.setKeepAliveStrategy((response, context) -> TimeUnit.MINUTES.toMillis(3)); return requestConfig.setDefaultCredentialsProvider(credentialsProvider); })); } } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/config/FilterConfig.java ================================================ package com.acimage.common.config; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.filter.CharacterEncodingFilter; import org.springframework.web.filter.HiddenHttpMethodFilter; @Configuration public class FilterConfig { /** * 设置编码 * @return */ @Bean public FilterRegistrationBean registerFilter1(){ FilterRegistrationBean registerFilter=new FilterRegistrationBean<>(); CharacterEncodingFilter encodeFilter=new CharacterEncodingFilter(); encodeFilter.setEncoding("UTF-8"); registerFilter.setFilter(encodeFilter); registerFilter.addUrlPatterns("/*"); return registerFilter; } /** * 设置HiddenHttpMethod,支持put、get、delete等请求 * @return */ @Bean public FilterRegistrationBean registerFilter2(){ FilterRegistrationBean registerFilter=new FilterRegistrationBean<>(); HiddenHttpMethodFilter hiddenHttpMethodFilter=new HiddenHttpMethodFilter(); registerFilter.setFilter(hiddenHttpMethodFilter); registerFilter.addUrlPatterns("/*"); return registerFilter; } } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/config/JacksonConfig.java ================================================ package com.acimage.common.config; import cn.hutool.core.date.DatePattern; import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class JacksonConfig { /** * Jackson全局转化long类型为String,解决前端接受long类型缺失精度问题 * @return Jackson2ObjectMapperBuilderCustomizer 注入的对象 */ @Bean public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() { return jacksonObjectMapperBuilder -> jacksonObjectMapperBuilder .serializerByType(Long.class, ToStringSerializer.instance) .serializerByType(Long.TYPE, ToStringSerializer.instance) .simpleDateFormat(DatePattern.NORM_DATETIME_PATTERN); } } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/config/PaginationConfig.java ================================================ package com.acimage.common.config; import com.baomidou.mybatisplus.annotation.DbType; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class PaginationConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(); paginationInnerInterceptor.setDbType(DbType.MYSQL); //页数溢出时跳到第一页 paginationInnerInterceptor.setOverflow(true); interceptor.addInnerInterceptor(paginationInnerInterceptor); return interceptor; } } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/config/RabbitmqConvertConfig.java ================================================ package com.acimage.common.config; import org.springframework.amqp.core.AcknowledgeMode; import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory; import org.springframework.amqp.rabbit.connection.ConnectionFactory; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.amqp.rabbit.listener.RabbitListenerContainerFactory; import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @ConditionalOnClass(RabbitTemplate.class) @Configuration public class RabbitmqConvertConfig { //发送方序列化 @Bean public RabbitTemplate jacksonRabbitTemplate(ConnectionFactory connectionFactory) { RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory); rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter()); return rabbitTemplate; } //消费者序列化 @Bean public RabbitListenerContainerFactory rabbitListenerContainerFactory(ConnectionFactory connectionFactory){ SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); factory.setConnectionFactory(connectionFactory); factory.setMessageConverter(new Jackson2JsonMessageConverter()); factory.setAcknowledgeMode(AcknowledgeMode.MANUAL); return factory; } } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/deprecated/IpInterceptorBak.java ================================================ package com.acimage.common.deprecated; import com.acimage.common.global.context.UserContext; import com.acimage.common.utils.IpUtils; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.concurrent.TimeUnit; /** * 获取请求的token状态 */ @Slf4j @Component public class IpInterceptorBak implements HandlerInterceptor { @Autowired StringRedisTemplate stringRedisTemplate; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String KEY_IP="acimage:ip:"; String KEY_BLACK_IP="acimage:ip:black"; long EXPIRE_SECONDS=10L; long limitTimes=300L; String ip = IpUtils.getIp(request); UserContext.setIp(ip); String ipKey=KEY_IP+ip; String blackIpKey=KEY_BLACK_IP+ip; Boolean isBlackIp=stringRedisTemplate.opsForSet().isMember(blackIpKey,ip); if(Boolean.TRUE.equals(isBlackIp)){ return false; } if (handler instanceof HandlerMethod) { Long visitNum=stringRedisTemplate.opsForValue().increment(ipKey); if(visitNum==null){ log.error("ip:{} 操作:ip访问计数 错误:redis中ip计数返回值为空",ip); } else if (visitNum==1){ stringRedisTemplate.expire(ipKey,EXPIRE_SECONDS, TimeUnit.SECONDS); } else if (visitNum>limitTimes) { //加入黑名单 stringRedisTemplate.opsForSet().add(blackIpKey,ip); response.setStatus(HttpStatus.FORBIDDEN.value()); return false; } } return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { HandlerInterceptor.super.postHandle(request, response, handler, modelAndView); } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { UserContext.remove(); } } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/deprecated/JwtInterceptorBak.java ================================================ package com.acimage.common.deprecated; import com.acimage.common.global.exception.NullTokenException; import com.acimage.common.deprecated.annotation.utils.AuthenticationUtils; import com.acimage.common.global.consts.HeaderKeyConstants; import com.acimage.common.global.context.UserContext; import com.acimage.common.global.enums.AuthenticationType; import com.acimage.common.global.enums.TokenStatus; import com.acimage.common.service.TokenService; import com.acimage.common.utils.IpUtils; import com.acimage.common.utils.JwtUtils; import com.auth0.jwt.exceptions.JWTVerificationException; import com.auth0.jwt.exceptions.TokenExpiredException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * 获取请求的token状态 */ public class JwtInterceptorBak implements HandlerInterceptor { @Autowired TokenService tokenService; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //设置ip String ip = IpUtils.getIp(request); UserContext.setIp(ip); if (handler instanceof HandlerMethod) { HandlerMethod handlerMethod=(HandlerMethod) handler; //获取该方法权限类型,优先注解顺序:方法、类 AuthenticationType authenticationType = AuthenticationUtils.getAuthenticationType(handlerMethod); //无登录权限要求则放行 if (authenticationType == null || authenticationType == AuthenticationType.NONE) { UserContext.setTokenStatus(TokenStatus.PASS_TOKEN_VERIFY); return true; } } //获取token String token = request.getHeader(HeaderKeyConstants.AUTHORIZATION); //验证token,验证不通过抛出异常 try { JwtUtils.verifyToken(token); } catch (NullTokenException e1) { UserContext.setTokenStatus(TokenStatus.NULL); return true; } catch (TokenExpiredException e2) { UserContext.setTokenStatus(TokenStatus.EXPIRE); return true; } catch (JWTVerificationException e3) { UserContext.setTokenStatus(TokenStatus.INVALID); return true; } if (!tokenService.hasRecorded(token)) { UserContext.setTokenStatus(TokenStatus.MISMATCH_IP); } else { UserContext.setTokenStatus(TokenStatus.VALID); } UserContext.saveCurrentUserInfo(JwtUtils.getUserId(token), JwtUtils.getUsername(token), JwtUtils.getPhotoUrl(token)); return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { HandlerInterceptor.super.postHandle(request, response, handler, modelAndView); } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { UserContext.remove(); } } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/deprecated/PermissionInterceptorBak.java ================================================ package com.acimage.common.deprecated; import com.acimage.common.deprecated.annotation.utils.AuthenticationUtils; import com.acimage.common.global.context.UserContext; import com.acimage.common.global.enums.AuthenticationType; import com.acimage.common.global.enums.TokenStatus; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.mvc.ParameterizableViewController; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * 验证token状态和请求所需权限是否匹配 */ @Slf4j public class PermissionInterceptorBak implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String ip = UserContext.getIp(); log.info("access 用户:{} token状态:{} 访问:{} {} ip:{}", UserContext.getUsername(), UserContext.getTokenStatus(), request.getRequestURI(), request.getMethod(), ip); TokenStatus tokenStatus = UserContext.getTokenStatus(); if (tokenStatus == TokenStatus.PASS_TOKEN_VERIFY) { return true; } //如果通过viewController访问页面 if (handler instanceof ParameterizableViewController) { if (!(tokenStatus == TokenStatus.VALID||tokenStatus == TokenStatus.MISMATCH_IP)) { response.sendRedirect("/"); return false; } } if (handler instanceof HandlerMethod) { HandlerMethod handlerMethod = (HandlerMethod) handler; //获取该方法权限类型,优先注解顺序:方法、类 AuthenticationType authenticationType = AuthenticationUtils.getAuthenticationType(handlerMethod); //无登录权限要求则放行 if (authenticationType == null || authenticationType == AuthenticationType.NONE) { return true; } else if (authenticationType == AuthenticationType.TOKEN_REQUIRED) { if (!(tokenStatus == TokenStatus.VALID||tokenStatus == TokenStatus.MISMATCH_IP)) { response.setStatus(HttpStatus.UNAUTHORIZED.value()); return false; } } } // ParameterizableViewController viewHandler=(ParameterizableViewController) handler; // viewHandler.getViewName(); // String token= CookieUtils.getValueByName(request.getCookies(), JwtUtils.KEY_TOKEN); return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { HandlerInterceptor.super.postHandle(request, response, handler, modelAndView); } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { //移除用户信息,防止之后用该线程的信息误判(因为线程池) UserContext.remove(); } } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/deprecated/QiniuUtils.java ================================================ package com.acimage.common.deprecated; import cn.hutool.core.collection.CollectionUtil; import cn.hutool.http.HttpUtil; import com.acimage.common.global.exception.BusinessException; import com.acimage.common.utils.ExceptionUtils; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.qiniu.cdn.CdnManager; import com.qiniu.cdn.CdnResult; import com.qiniu.common.QiniuException; import com.qiniu.http.Client; import com.qiniu.http.Response; import com.qiniu.storage.BucketManager; import com.qiniu.storage.Configuration; import com.qiniu.storage.Region; import com.qiniu.storage.UploadManager; import com.qiniu.storage.model.BatchStatus; import com.qiniu.storage.model.DefaultPutRet; import com.qiniu.util.Auth; import com.qiniu.util.StringMap; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; import org.springframework.web.multipart.MultipartFile; import javax.annotation.Nullable; import javax.annotation.PostConstruct; import javax.validation.constraints.NotNull; import java.io.*; import java.net.URLEncoder; import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.Date; import java.util.List; @Deprecated @Slf4j @ConditionalOnClass(Auth.class) @ConfigurationProperties(prefix = "qiniu") public class QiniuUtils { private String accessKey; private String secretKey; private String bucket; private String domain; private String uploadToken; UploadManager uploadManager; Auth auth; Configuration cfg; private static final ObjectMapper mapper = new ObjectMapper(); @PostConstruct private void init() { cfg = new Configuration(Region.huanan()); cfg.resumableUploadAPIVersion = Configuration.ResumableUploadAPIVersion.V2;// 指定分片上传版本 uploadManager = new UploadManager(cfg); auth = Auth.create(accessKey, secretKey); uploadToken = auth.uploadToken(bucket); // System.out.println(accessKey); // System.out.println(secretKey); // System.out.println(bucket); // System.out.println(domain); } public void setAccessKey(String accessKey) { this.accessKey = accessKey; } public void setSecretKey(String secretKey) { this.secretKey = secretKey; } public void setBucket(String bucket) { this.bucket = bucket; } public void setDomain(String domain) { this.domain = domain; } /** * @param multipartFile 上传的图片 */ public void upload(@NotNull MultipartFile multipartFile, String urlWithoutDomain) { InputStream is; try { is = multipartFile.getInputStream(); } catch (IOException e) { ExceptionUtils.printIfDev(e); log.error("error: multipartFile.getInputStream()异常:{}", e.getMessage()); throw new RuntimeException(e); } putAndLog(is, urlWithoutDomain, uploadToken); } public void upload(@NotNull File file, String urlWithoutDomain) { putAndLog(file, urlWithoutDomain, uploadToken); } public void cover(@NotNull MultipartFile multipartFile, String urlWithoutDomain) { String coverToken = auth.uploadToken(bucket, urlWithoutDomain); InputStream is; try { is = multipartFile.getInputStream(); } catch (IOException e) { throw new RuntimeException(e); } putAndLog(is, urlWithoutDomain, coverToken); new Thread(() -> { refreshFile(urlWithoutDomain); refreshQuery(urlWithoutDomain); }).start(); } public void refreshFile(String... urls) { int urlAmountLimit = 100; CdnManager c = new CdnManager(auth); if (urls.length > urlAmountLimit) { log.error("调用刷新的链接超过100个"); return; } //得到真实链接 for (int i = 0; i < urls.length; i++) { urls[i] = getTrueUrl(urls[i]); } try { //单次方法调用刷新的链接不可以超过100个 CdnResult.RefreshResult result = c.refreshUrls(urls); //获取其他的回复内容 } catch (QiniuException e) { log.error("刷新七牛云url失败 urls:{}", Arrays.asList(urls)); } } public void refreshQuery(String url) { String trueUrl = getTrueUrl(url); StringMap str = auth.authorization(trueUrl); Client client = new Client(); try { client.post(trueUrl, "", str); } catch (QiniuException e) { log.error("刷新查询oss url失败 url:{}", url); ExceptionUtils.printIfDev(e); } } public void deleteFile(String url) { BucketManager bucketManager = new BucketManager(auth, cfg); try { bucketManager.delete(bucket, url); } catch (QiniuException ex) { //如果遇到异常,说明删除失败 log.error("云文件删除失败 url:{} 返回代码:{} 返回错误信息:{}", url, ex.code(), ex.response.toString()); } } public void batchDelete(List urlList) { if (CollectionUtil.isEmpty(urlList)) { return; } BucketManager bucketManager = new BucketManager(auth, cfg); try { //单次批量请求的文件数量不得超过1000 String[] urls = urlList.toArray(new String[0]); BucketManager.BatchOperations batchOperations = new BucketManager.BatchOperations(); batchOperations.addDeleteOp(bucket, urls); Response response = bucketManager.batch(batchOperations); BatchStatus[] batchStatusList = response.jsonToObject(BatchStatus[].class); for (int i = 0; i < urls.length; i++) { BatchStatus status = batchStatusList[i]; String key = urls[i]; System.out.print(key + "\t"); if (status.code == 200) { System.out.println("delete success"); } else { System.out.println(status.data.error); } } } catch (QiniuException ex) { System.err.println(ex.response.toString()); } } public String generateUrl(String suffix, Date uploadTime, @Nullable String prefix) { String formatPattern = "yyyy/MM/dd"; String newPrefix = prefix == null ? "" : prefix + "/"; SimpleDateFormat formatter = new SimpleDateFormat(formatPattern); return newPrefix + formatter.format(uploadTime) + "/" + suffix; } public String getTrueUrl(String urlWithoutDomain) { return domain + "/" + urlWithoutDomain; } public void download(String url, String toPath) { String encodedUrl = null; try { encodedUrl = URLEncoder.encode(url, "utf-8").replace("+", "%20"); } catch (UnsupportedEncodingException e) { ExceptionUtils.printIfDev(e); log.error("url编码失败 error:{}", e.getLocalizedMessage()); } String publicUrl = domain + "/" + encodedUrl; HttpUtil.downloadFile(publicUrl, new File(toPath));//下载 } private void putAndLog(Object inputStreamOrFile, String urlWithoutDomain, String token) { Response response = null; try { if (inputStreamOrFile instanceof InputStream ) { InputStream is=(InputStream) inputStreamOrFile; response = uploadManager.put(is, urlWithoutDomain, token, null, null); } else if (inputStreamOrFile instanceof File) { File file=(File) inputStreamOrFile; response = uploadManager.put(file, urlWithoutDomain, token); } else { throw new IllegalArgumentException(String.format("参数inputStreamOrFile类型错误:%s", inputStreamOrFile.getClass())); } if (response == null) { return; } //解析上传成功的结果 DefaultPutRet putRet = mapper.readValue(response.bodyString(), DefaultPutRet.class); log.info("文件上传七牛云成功 返回结果key:{} hash:{}", putRet.key, putRet.hash); } catch (QiniuException ex) { Response r = ex.response; System.err.println(r.toString()); try { String responseMsg = r.bodyString(); log.error(responseMsg); throw new BusinessException("服务器上传文件失败"); } catch (QiniuException ex2) { log.error("文件上传后返回信息读取失败 url:{}", urlWithoutDomain); throw new BusinessException("上传文件出错了"); //ignore } } catch (JsonProcessingException e) { log.error("json解析七牛云返回结果异常"); throw new BusinessException("上传文件出错了,服务器解析结果异常"); } } } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/deprecated/QiniuUtilsBak.java ================================================ package com.acimage.common.deprecated; import cn.hutool.core.collection.CollectionUtil; import cn.hutool.http.HttpUtil; import com.acimage.common.global.exception.BusinessException; import com.acimage.common.utils.ExceptionUtils; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.qiniu.cdn.CdnManager; import com.qiniu.cdn.CdnResult; import com.qiniu.common.QiniuException; import com.qiniu.http.Client; import com.qiniu.http.Response; import com.qiniu.storage.BucketManager; import com.qiniu.storage.Configuration; import com.qiniu.storage.Region; import com.qiniu.storage.UploadManager; import com.qiniu.storage.model.BatchStatus; import com.qiniu.storage.model.DefaultPutRet; import com.qiniu.util.Auth; import com.qiniu.util.StringMap; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; import org.springframework.web.multipart.MultipartFile; import javax.annotation.Nullable; import javax.annotation.PostConstruct; import javax.validation.constraints.NotNull; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.Date; import java.util.List; @Slf4j @Component @ConditionalOnClass(Auth.class) @ConfigurationProperties(prefix = "qiniu") @Deprecated public class QiniuUtilsBak { private String accessKey; private String secretKey; private String bucket; private String domain; private String uploadToken; UploadManager uploadManager; Auth auth; Configuration cfg; private static final ObjectMapper mapper = new ObjectMapper(); @PostConstruct private void init() { cfg = new Configuration(Region.huanan()); cfg.resumableUploadAPIVersion = Configuration.ResumableUploadAPIVersion.V2;// 指定分片上传版本 uploadManager = new UploadManager(cfg); auth = Auth.create(accessKey, secretKey); uploadToken = auth.uploadToken(bucket); } public void setAccessKey(String accessKey) { this.accessKey = accessKey; } public void setSecretKey(String secretKey) { this.secretKey = secretKey; } public void setBucket(String bucket) { this.bucket = bucket; } public void setDomain(String domain) { this.domain = domain; } /** * @param multipartFile 上传的图片 */ public void upload(@NotNull MultipartFile multipartFile, String urlWithoutDomain) { InputStream is; try { is = multipartFile.getInputStream(); } catch (IOException e) { ExceptionUtils.printIfDev(e); log.error("error: multipartFile.getInputStream()异常:{}", e.getMessage()); throw new RuntimeException(e); } putAndLog(is, urlWithoutDomain, uploadToken); } public void upload(@NotNull File file, String urlWithoutDomain) { putAndLog(file, urlWithoutDomain, uploadToken); } public void cover(@NotNull MultipartFile multipartFile, String urlWithoutDomain) { String coverToken = auth.uploadToken(bucket, urlWithoutDomain); InputStream is; try { is = multipartFile.getInputStream(); } catch (IOException e) { throw new RuntimeException(e); } putAndLog(is, urlWithoutDomain, coverToken); new Thread(() -> { refreshFile(urlWithoutDomain); refreshQuery(urlWithoutDomain); }).start(); } public void refreshFile(String... urls) { int urlAmountLimit = 100; CdnManager c = new CdnManager(auth); if (urls.length > urlAmountLimit) { log.error("调用刷新的链接超过100个"); return; } //得到真实链接 for (int i = 0; i < urls.length; i++) { urls[i] = getTrueUrl(urls[i]); } try { //单次方法调用刷新的链接不可以超过100个 CdnResult.RefreshResult result = c.refreshUrls(urls); //获取其他的回复内容 } catch (QiniuException e) { log.error("刷新七牛云url失败 urls:{}", Arrays.asList(urls)); } } public void refreshQuery(String url) { String trueUrl = getTrueUrl(url); StringMap str = auth.authorization(trueUrl); Client client = new Client(); try { client.post(trueUrl, "", str); } catch (QiniuException e) { log.error("刷新查询oss url失败 url:{} error:{}", url,e.getMessage()); ExceptionUtils.printIfDev(e); } } public void deleteFile(String url) { BucketManager bucketManager = new BucketManager(auth, cfg); try { bucketManager.delete(bucket, url); } catch (QiniuException ex) { //如果遇到异常,说明删除失败 log.error("云文件删除失败 url:{} 返回代码:{} 返回错误信息:{}", url, ex.code(), ex.response.toString()); } } public void batchDelete(List urlList) { if (CollectionUtil.isEmpty(urlList)) { return; } BucketManager bucketManager = new BucketManager(auth, cfg); try { //单次批量请求的文件数量不得超过1000 String[] urls = urlList.toArray(new String[0]); BucketManager.BatchOperations batchOperations = new BucketManager.BatchOperations(); batchOperations.addDeleteOp(bucket, urls); Response response = bucketManager.batch(batchOperations); BatchStatus[] batchStatusList = response.jsonToObject(BatchStatus[].class); for (int i = 0; i < urls.length; i++) { BatchStatus status = batchStatusList[i]; String key = urls[i]; System.out.print(key + "\t"); if (status.code == 200) { System.out.println("delete success"); } else { System.out.println(status.data.error); } } } catch (QiniuException ex) { System.err.println(ex.response.toString()); } } public String generateUrl(String suffix, Date uploadTime, @Nullable String prefix) { String formatPattern = "yyyy/MM/dd"; String newPrefix = prefix == null ? "" : prefix + "/"; SimpleDateFormat formatter = new SimpleDateFormat(formatPattern); return newPrefix + formatter.format(uploadTime) + "/" + suffix; } public String getTrueUrl(String urlWithoutDomain) { return domain + "/" + urlWithoutDomain; } public void download(String url, String toPath) { String encodedUrl = null; try { encodedUrl = URLEncoder.encode(url, "utf-8").replace("+", "%20"); } catch (UnsupportedEncodingException e) { ExceptionUtils.printIfDev(e); log.error("url编码失败 error:{}", e.getMessage()); } String publicUrl = domain + "/" + encodedUrl; long expireInSeconds = 3600;//1小时,可以自定义链接过期时间 HttpUtil.downloadFile(publicUrl, new File(toPath));//下载 } private void putAndLog(Object inputStreamOrFile, String urlWithoutDomain, String token) { Response response = null; try { if (inputStreamOrFile instanceof InputStream) { InputStream is = (InputStream) inputStreamOrFile; response = uploadManager.put(is, urlWithoutDomain, token, null, null); } else if (inputStreamOrFile instanceof File) { File file = (File) inputStreamOrFile; response = uploadManager.put(file, urlWithoutDomain, token); } else { throw new IllegalArgumentException(String.format("参数inputStreamOrFile类型错误:%s", inputStreamOrFile.getClass())); } if (response == null) { return; } //解析上传成功的结果 DefaultPutRet putRet = mapper.readValue(response.bodyString(), DefaultPutRet.class); log.info("文件上传七牛云成功 返回结果key:{} hash:{}", putRet.key, putRet.hash); } catch (QiniuException ex) { Response r = ex.response; System.err.println(r.toString()); try { String responseMsg = r.bodyString(); log.error(responseMsg); throw new BusinessException("服务器上传文件失败"); } catch (QiniuException ex2) { log.error("文件上传后返回信息读取失败 url:{}", urlWithoutDomain); throw new BusinessException("上传文件出错了"); //ignore } } catch (JsonProcessingException e) { log.error("json解析七牛云返回结果异常"); throw new BusinessException("上传文件出错了,服务器解析结果异常"); } } } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/deprecated/UserCommunityStatistic.java ================================================ package com.acimage.common.deprecated; import com.acimage.common.model.domain.community.CmtyUser; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor @AllArgsConstructor @Deprecated public class UserCommunityStatistic { @TableId Long userId; Integer topicCount; Integer starCount; @TableField(exist = false) CmtyUser cmtyUser; } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/deprecated/annotation/Authentication.java ================================================ package com.acimage.common.deprecated.annotation; import com.acimage.common.global.enums.AuthenticationType; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 注解上的类或方法表示不需要登录后才能访问 */ @Target({ElementType.METHOD,ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface Authentication { AuthenticationType type() default AuthenticationType.TOKEN_REQUIRED; } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/deprecated/annotation/utils/AuthenticationUtils.java ================================================ package com.acimage.common.deprecated.annotation.utils; import com.acimage.common.deprecated.annotation.Authentication; import com.acimage.common.global.enums.AuthenticationType; import org.springframework.web.method.HandlerMethod; public class AuthenticationUtils { public static AuthenticationType getAuthenticationType(HandlerMethod handlerMethod){ if(handlerMethod==null){ return null; } //判断方法上是否有Authorization注解 Authentication methodAuthentication =handlerMethod.getMethod().getAnnotation(Authentication.class); if(methodAuthentication !=null){ return methodAuthentication.type(); } //获取类上的Authorization注解 Authentication classAuthentication =handlerMethod.getBeanType().getAnnotation(Authentication.class); if(classAuthentication !=null){ return classAuthentication.type(); } return null; } } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/deprecated/typehandler/MatchRuleTypeHandler.java ================================================ package com.acimage.common.deprecated.typehandler; import com.acimage.common.global.enums.MatchRule; import org.apache.ibatis.type.BaseTypeHandler; import org.apache.ibatis.type.JdbcType; import org.apache.ibatis.type.MappedJdbcTypes; import org.apache.ibatis.type.MappedTypes; import java.sql.CallableStatement; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; //@MappedTypes(MatchRule.class) public class MatchRuleTypeHandler extends BaseTypeHandler { @Override public void setNonNullParameter(PreparedStatement preparedStatement, int i, MatchRule matchRule, JdbcType jdbcType) throws SQLException { preparedStatement.setInt(i, matchRule.getKey()); } @Override public MatchRule getNullableResult(ResultSet resultSet, String column) throws SQLException { return MatchRule.from(resultSet.getInt(column)); } @Override public MatchRule getNullableResult(ResultSet resultSet, int i) throws SQLException { return MatchRule.from(Integer.parseInt(resultSet.getString(i))); } @Override public MatchRule getNullableResult(CallableStatement callableStatement, int i) throws SQLException { return MatchRule.from(Integer.parseInt(callableStatement.getString(i))); } } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/global/aop/LogAdvice.java ================================================ package com.acimage.common.global.aop; import com.acimage.common.utils.common.AopUtils; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import org.springframework.web.bind.annotation.GetMapping; import java.lang.reflect.Method; import java.lang.reflect.Parameter; @Aspect @Component @Slf4j @Order(200) public class LogAdvice { private static final int MAX_RETURN_VALUE_LENGTH = 100; // @Pointcut("execution(public * com.acimage..*.controller.*Controller.*(..))") @Pointcut("execution(public * com.acimage..*.*Controller.*(..))") public void controllerPointCut() { } @Pointcut("execution(public * com.acimage..*.*Provider.*(..))") public void providerPointCut() { } /** * 记录每个接口出异常时时的入参 */ @Around("controllerPointCut() || providerPointCut()") private Object logParametersAndReturnValue(ProceedingJoinPoint joinPoint) throws Throwable { Method method = AopUtils.methodOf(joinPoint); Object[] args = joinPoint.getArgs(); Parameter[] parameters = method.getParameters(); long startTime = System.currentTimeMillis(); //记录入参 StringBuilder argsString = new StringBuilder(); argsString.append(method.getName()); argsString.append(" 入参-->"); if (args != null) { for (int i = 0; i < args.length; i++) { if (i < parameters.length) { argsString.append(parameters[i].getName()); argsString.append(": "); } argsString.append(args[i]); argsString.append(" "); } } Object obj; try { obj = joinPoint.proceed(); } catch (Throwable e) { log.error("出异常: " + argsString); throw e; } if (!method.isAnnotationPresent(GetMapping.class)) { String returnValue = obj == null ? "" : obj.toString(); log.info(method.getName() + " 返回值-->" + returnValue); } String classMethod = method.getName() + " " + method.getDeclaringClass().getSimpleName(); long cost = System.currentTimeMillis() - startTime; if (cost > 500) { log.warn("{}耗时 {}ms", classMethod, cost); } else { log.debug("{}耗时 {}ms", classMethod, cost); } return obj; } } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/global/consts/EsConstants.java ================================================ package com.acimage.common.global.consts; public class EsConstants { public static final String IK_SMART="ik_smart"; public static final String IK_MAX_WORD="ik_max_word"; } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/global/consts/FileFormatConstants.java ================================================ package com.acimage.common.global.consts; import java.util.Arrays; import java.util.List; public class FileFormatConstants { public static List ALLOWED_IMAGE_FORMAT = Arrays.asList("jpeg", "jpg", "png"); public static List ALLOWED_CAROUSEL_FORMAT = Arrays.asList("jpeg", "jpg", "png","webp"); public static List ALLOWED_COVER_IMAGE_FORMAT = Arrays.asList("jpeg", "jpg", "png","webp"); public static final String WEBP="webp"; public static final String WEBP_CONTENT_TYPE="image/webp"; } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/global/consts/HeaderKeyConstants.java ================================================ package com.acimage.common.global.consts; public class HeaderKeyConstants { public static final String AUTHORIZATION="authorization"; public static final String FEIGN_X_USER_IP ="x-origin-user-ip"; public static final String SEC_WEBSOCKET_PROTOCOL="Sec-WebSocket-Protocol"; } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/global/consts/JwtConstants.java ================================================ package com.acimage.common.global.consts; public class JwtConstants { public static final int USER_EXPIRE_DAYS = 2; public static final int ADMIN_EXPIRE_DAYS = 1; public static final String KEY_USERNAME="username"; public static final String KEY_USER_ID="userId"; public static final String KEY_PHOTO_URL="photoUrl"; } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/global/consts/MqConstants.java ================================================ package com.acimage.common.global.consts; public class MqConstants { public static final String HASH_IMAGE_QUEUE = "hash-image-queue"; public static final String HASH_IMAGE_ROUTE = "hash-image-route"; public static final String REMOVE_TOPIC_IMAGES_QUEUE = "remove-topic-images-queue"; public static final String REMOVE_TOPIC_IMAGE_ROUTE = "remove-topic-images-route"; public static final String SYNC_IMAGES_QUEUE="sync-images-queue"; public static final String SYNC_IMAGES_ROUTE="sync-images-route"; public static final String SYNC_ES_QUEUE = "sync-es-queue"; public static final String SYNC_ES_ROUTE = "sync-es-route"; public static final String SYNC_USER_QUEUE = "sync-user-queue"; public static final String SYNC_USER_ROUTE = "sync-user-route"; public static final String USER_MSG_QUEUE="user-msg-queue"; public static final String USER_MSG_ROUTE="user-msg-route"; public static final String TOPIC_IMAGES_EXCHANGE = "topic-images-exchange"; public static final String SYNC_ES_EXCHANGE = "sync-es-exchange"; public static final String COMMUNITY_USER_EXCHANGE="community-user-exchange"; } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/global/consts/StorePrefixConstants.java ================================================ package com.acimage.common.global.consts; public class StorePrefixConstants { public final static String TOPIC_IMAGE="topicImage"; public final static String USER_PHOTO="userPhoto"; public final static String COVER_IMAGE="coverImage"; public final static String HOME_CAROUSEL="homeCarousel"; } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/global/consts/SysKeyConstants.java ================================================ package com.acimage.common.global.consts; public class SysKeyConstants { /** * 记录网站浏览量 */ public static final String LOGK_PAGE_VIEW="acimage:admin:websiteData:pageView"; /** * 记录接口访问次数 */ public static final String STRINGK_INTERFACE_TOTAL="acimage:admin:websiteData:interface:total"; public static final String STRINGK_INTERFACE_SUCCESS="acimage:admin:websiteData:interface:success"; public static final String STRINGK_INTERFACE_FAILURE="acimage:admin:websiteData:interface:failure"; } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/global/consts/TimeConstants.java ================================================ package com.acimage.common.global.consts; public class TimeConstants { public static final long DAY_SECONDS=24*60*60; } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/global/context/UserContext.java ================================================ package com.acimage.common.global.context; import com.acimage.common.global.enums.TokenStatus; public class UserContext { private static final ThreadLocal userId = new ThreadLocal<>(); private static final ThreadLocal username = new ThreadLocal<>(); private static final ThreadLocal photoUrl = new ThreadLocal<>(); private static final ThreadLocal tokenStatus = new ThreadLocal<>(); private static final ThreadLocal ip = new ThreadLocal<>(); public static void saveCurrentUserInfo(Long userId, String username, String photoUrl) { UserContext.userId.set(userId); UserContext.username.set(username); UserContext.photoUrl.set(photoUrl); } /** * 移除登录用户信息,在拦截器方法afterCompletion中,应移除当前用户对象 */ public static void remove() { userId.remove(); username.remove(); photoUrl.remove(); tokenStatus.remove(); ip.remove(); } public static void setTokenStatus(TokenStatus tokenStatus) { UserContext.tokenStatus.set(tokenStatus); } public static void setUserId(Long userId){ UserContext.userId.set(userId); } public static void setUsername(String username){ UserContext.username.set(username); } public static void setIp(String ip) { UserContext.ip.set(ip); } public static Long getUserId() { return userId.get(); } public static String getUsername() { return username.get(); } public static String getPhotoUrl() { return photoUrl.get(); } public static TokenStatus getTokenStatus() { return tokenStatus.get(); } public static String getIp() { return ip.get(); } } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/global/enums/AuthenticationType.java ================================================ package com.acimage.common.global.enums; public enum AuthenticationType { NONE(0), TOKEN_REQUIRED(1), ADMIN(2); private final int key; AuthenticationType(int key) { this.key = key; } public int getKey() { return this.key; } } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/global/enums/MatchRule.java ================================================ package com.acimage.common.global.enums; /** * interface表中匹配规则 */ public enum MatchRule { /** * 精确匹配 */ EXACT(1), /** * 匹配前缀 */ PREFIX(2); private final int key; MatchRule(int key) { this.key = key; } public int getKey() { return this.key; } public static MatchRule from(int key){ for(MatchRule matchRule:MatchRule.values()){ if(matchRule.getKey()==key){ return matchRule; } } return null; } } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/global/enums/MyHttpMethod.java ================================================ package com.acimage.common.global.enums; import org.springframework.http.HttpMethod; public enum MyHttpMethod { GET, POST, PUT, DELETE, ALL; public static MyHttpMethod from(HttpMethod method) { try { return MyHttpMethod.valueOf(method.toString()); } catch (IllegalArgumentException e) { return null; } } } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/global/enums/ServiceType.java ================================================ package com.acimage.common.global.enums; public enum ServiceType { ADD, DELETE, UPDATE } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/global/enums/TokenStatus.java ================================================ package com.acimage.common.global.enums; public enum TokenStatus { PASS_TOKEN_VERIFY(0), NULL(1), EXPIRE(2), INVALID(3), MISMATCH_IP(4), VALID(5); private final int key; TokenStatus(int key) { this.key = key; } public int getKey() { return this.key; } } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/global/exception/BaseException.java ================================================ package com.acimage.common.global.exception; import com.acimage.common.result.Result; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @Getter @Setter @NoArgsConstructor @AllArgsConstructor public abstract class BaseException extends RuntimeException{ Integer code; String msg; public Result asResult(){ return new Result(code,null,msg); } } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/global/exception/BusinessException.java ================================================ package com.acimage.common.global.exception; import com.acimage.common.result.Code; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.Setter; @Getter @Setter @AllArgsConstructor public class BusinessException extends BaseException{ public BusinessException(String msg){ this.msg=msg; this.code= Code.ERR; } public BusinessException(Integer code, String msg) { super(code, msg); } } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/global/exception/NullTokenException.java ================================================ package com.acimage.common.global.exception; import com.auth0.jwt.exceptions.JWTVerificationException; public class NullTokenException extends JWTVerificationException { public NullTokenException(String message){super(message);} } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/global/exception/SystemException.java ================================================ package com.acimage.common.global.exception; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.Setter; @Getter @Setter @AllArgsConstructor public class SystemException extends BaseException{ } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/model/Index/TopicIndex.java ================================================ package com.acimage.common.model.Index; import cn.hutool.core.date.DatePattern; import com.acimage.common.global.consts.EsConstants; import com.acimage.common.model.domain.community.CmtyUser; import com.acimage.common.model.domain.community.Tag; import com.acimage.common.model.domain.community.Topic; import com.acimage.common.model.domain.user.User; import com.acimage.common.model.page.MyPage; import com.acimage.common.utils.common.BeanUtils; import com.acimage.common.utils.common.ListUtils; import com.fasterxml.jackson.annotation.JsonFormat; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import org.springframework.data.annotation.Id; import org.springframework.data.elasticsearch.annotations.*; import java.util.Date; import java.util.List; import java.util.stream.Collectors; @NoArgsConstructor @AllArgsConstructor @Builder @Data @Document(indexName = "topic") @Setting(replicas = 0) public class TopicIndex { @Id private Long id; @Field(type = FieldType.Text) private String all; @Field(type = FieldType.Keyword, store = true) private Long userId; @Field(type = FieldType.Text, analyzer = EsConstants.IK_MAX_WORD, store = true) private String username; @Field(type = FieldType.Keyword, index = false, store = true) private String photoUrl; @Field(type = FieldType.Text, analyzer = EsConstants.IK_MAX_WORD, store = true, copyTo = "all") private String content; @Field(type = FieldType.Text, analyzer = EsConstants.IK_MAX_WORD, store = true, copyTo = "all") private String title; @Field(type = FieldType.Integer, store = true) private Integer starCount; @Field(type = FieldType.Integer, store = true) private Integer pageView; @Field(type = FieldType.Integer, store = true) private Integer commentCount; @Field(store = true, index = false, type = FieldType.Keyword) private String coverImageUrl; @Field(type = FieldType.Date, pattern = DatePattern.NORM_DATETIME_PATTERN, store = true) private Date createTime; @Field(type = FieldType.Date, pattern = DatePattern.NORM_DATETIME_PATTERN, store = true) private Date updateTime; @Field(type = FieldType.Date, pattern = DatePattern.NORM_DATETIME_PATTERN, store = true) private Date activityTime; @Field(type = FieldType.Keyword, store = true) List tagIds; @Field(type = FieldType.Keyword, store = true) Integer categoryId; public static TopicIndex from(Topic topic) { TopicIndex topicIndex = BeanUtils.copyPropertiesTo(topic, TopicIndex.class); CmtyUser user = topic.getUser(); if (user != null) { topicIndex.setUsername(user.getUsername()); topicIndex.setPhotoUrl(user.getPhotoUrl()); } return topicIndex; } public static Topic toTopic(TopicIndex topicIndex) { Topic topic = BeanUtils.copyPropertiesTo(topicIndex, Topic.class); CmtyUser user = new CmtyUser(); user.setUsername(topicIndex.getUsername()); user.setPhotoUrl(topicIndex.getPhotoUrl()); topic.setUser(user); return topic; } public static List toTopicList(List topicIndexList) { return topicIndexList.stream() .map(TopicIndex::toTopic) .collect(Collectors.toList()); } public static MyPage toTopicPage(MyPage topicPage) { List topicList = toTopicList(topicPage.getDataList()); return new MyPage<>(topicPage.getTotalCount(),topicList); } } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/model/domain/community/Category.java ================================================ package com.acimage.common.model.domain.community; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableLogic; import com.fasterxml.jackson.annotation.JsonFormat; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.util.Date; @Data @NoArgsConstructor @AllArgsConstructor public class Category { public static final int LABEL_MAX=50; public static final int LABEL_MIN =2; public static final String LABEL_VALIDATION_MSG ="角色名在"+ LABEL_MIN +"-"+LABEL_MAX+"之间"; @TableId(type = IdType.AUTO) Integer id; String label; private Date createTime; private Date updateTime; @TableLogic(delval = "1") boolean deleted; } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/model/domain/community/CmtyUser.java ================================================ package com.acimage.common.model.domain.community; import com.baomidou.mybatisplus.annotation.TableId; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor @AllArgsConstructor public class CmtyUser { @TableId Long id; String username; String photoUrl; Integer topicCount; Integer starCount; } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/model/domain/community/Comment.java ================================================ package com.acimage.common.model.domain.community; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableLogic; import com.fasterxml.jackson.annotation.JsonFormat; import lombok.Data; import lombok.NoArgsConstructor; import java.util.Date; @Data @NoArgsConstructor public class Comment { public static final int CONTENT_MIN=2; public static final int CONTENT_MAX=200; public static final String CONTENT_VALIDATION_MSG="评论字数在"+CONTENT_MIN+"-"+CONTENT_MAX+"之间"; @TableId(type= IdType.INPUT) private Long id; private Long topicId; private Long userId; private String content; @TableLogic private boolean deleted; private Date createTime; private Date updateTime; @TableField(exist = false) Topic topic; @TableField(exist = false) CmtyUser user; } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/model/domain/community/HomeCarousel.java ================================================ package com.acimage.common.model.domain.community; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.fasterxml.jackson.annotation.JsonFormat; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.util.Date; @Data @AllArgsConstructor @NoArgsConstructor public class HomeCarousel { public static final int DESC_MIN = 2; public static final int DESC_MAX = 30; public static final String DESC_INVALID_MSG ="图片描述字数在"+ DESC_MIN +"-"+ DESC_MAX +"之间"; public static final int LINK_MAX = 100; public static final String LINK_INVALID_MSG ="图片描述字数在"+ 0 +"-"+ LINK_MAX +"之间"; @TableId(type= IdType.AUTO) private Integer id; private String description; private String url; private String link; private Integer location; private Integer size; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8") private Date createTime; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8") private Date updateTime; } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/model/domain/community/Star.java ================================================ package com.acimage.common.model.domain.community; import com.acimage.common.model.domain.community.Topic; import com.baomidou.mybatisplus.annotation.TableField; import com.fasterxml.jackson.annotation.JsonFormat; import lombok.Data; import lombok.NoArgsConstructor; import java.util.Date; @Data @NoArgsConstructor public class Star { private Long userId; private Long topicId; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date createTime; @TableField(exist = false) Topic topic; public Star(Long userId, Long topicId) { this.userId = userId; this.topicId = topicId; } } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/model/domain/community/Tag.java ================================================ package com.acimage.common.model.domain.community; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableLogic; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import java.util.Date; @Data @NoArgsConstructor @AllArgsConstructor public class Tag { @TableId(type = IdType.AUTO) Integer id; String label; private Date createTime; private Date updateTime; @TableLogic(delval = "1") boolean deleted; } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/model/domain/community/TagTopic.java ================================================ package com.acimage.common.model.domain.community; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableLogic; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import java.util.Date; @Data @NoArgsConstructor @AllArgsConstructor @Builder public class TagTopic { @TableId(type = IdType.AUTO) Long id; Long topicId; Integer tagId; private Date createTime; @TableLogic(delval = "1") boolean deleted; } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/model/domain/community/Topic.java ================================================ package com.acimage.common.model.domain.community; import com.acimage.common.model.domain.user.User; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableLogic; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import java.util.Date; import java.util.List; @NoArgsConstructor @AllArgsConstructor @Builder @Data public class Topic { public static final int CONTENT_MIN = 4; public static final int CONTENT_MAX = 500; public static final String CONTENT_VALIDATION_MSG= "内容长度在" + CONTENT_MIN + "-" + CONTENT_MAX + "之间"; public static final int TITLE_MIN = 4; public static final int TITLE_MAX = 30; public static final String TITLE_VALIDATION_MSG= "标题长度在"+TITLE_MIN+"-"+TITLE_MAX+"之间"; public static int IMAGES_AMOUNT_MAX=5; public static int IMAGES_AMOUNT_MIN=1; public static final String IMAGE_VALIDATION_MSG= "图片数量在"+IMAGES_AMOUNT_MIN+"-"+IMAGES_AMOUNT_MAX+"之间"; public static int TAG_MAX=3; public static int TAG_MIN=1; public static final String TAG_VALIDATION_MSG= "标签数量在"+TAG_MIN+"-"+TAG_MAX+"之间"; @TableId(type= IdType.INPUT) private Long id; private Long userId; private String content; private String title; private Integer starCount; private Integer pageView; private Integer commentCount; private String coverImageUrl; private Integer categoryId; private Date activityTime; private Date createTime; private Date updateTime; @TableLogic(delval = "1") boolean deleted; @TableField(exist = false) CmtyUser user; @TableField(exist = false) Category category; @TableField(exist = false) List tagIds; // @TableField(exist = false) // List images; @TableField(exist = false) List comments; } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/model/domain/community/TopicHtml.java ================================================ package com.acimage.common.model.domain.community; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableLogic; import com.fasterxml.jackson.annotation.JsonFormat; import lombok.Data; import lombok.NoArgsConstructor; import java.util.Date; @NoArgsConstructor @Data public class TopicHtml { public static final int HTML_MIN = 4; public static final int HTML_MAX = 4500; public static final String HTML_VALIDATION_MSG = "html源码长度在" + HTML_MIN + "-" + HTML_MAX + "之间"; @TableId(type= IdType.INPUT) private Long topicId; String html; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8") private Date createTime; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8") private Date updateTime; @TableLogic(delval = "1") boolean deleted; } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/model/domain/image/Image.java ================================================ package com.acimage.common.model.domain.image; import com.acimage.common.model.domain.community.Topic; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableLogic; import com.fasterxml.jackson.annotation.JsonFormat; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import java.util.Date; @NoArgsConstructor @AllArgsConstructor @Builder @Data public class Image { public static final int DESCRIPTION_MIN = 2; public static final int DESCRIPTION_MAX = 30; public static final String DESCRIPTION_VALIDATION_MSG ="图片描述字数在"+DESCRIPTION_MIN+"-"+DESCRIPTION_MAX+"之间"; public static final int FILE_NAME_MIN = 3; public static final int FILE_NAME_MAX = 80; public static final String FILE_NAME_VALIDATION_MSG ="文件名在"+FILE_NAME_MIN+"-"+FILE_NAME_MAX+"之间"; @TableId(type= IdType.INPUT) private Long id; private Long topicId; private Integer size; private String fileName; private String description; private String url; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8") private Date createTime; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8") private Date updateTime; @TableLogic private boolean deleted; @TableField(exist = false) Topic topic; public Image(Long id, Long topicId, Integer size, String description) { this.id = id; this.topicId = topicId; this.size = size; this.description = description; } } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/model/domain/image/ImageHash.java ================================================ package com.acimage.common.model.domain.image; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.util.Date; @Data @AllArgsConstructor @NoArgsConstructor public class ImageHash { @TableId(type= IdType.INPUT) Long imageId; Long hashValue; Integer hashSum; Date createTime; @TableField(exist = false) Integer distance; public ImageHash(Long imageId, Long hashValue, Integer hashSum) { this.imageId = imageId; this.hashValue = hashValue; this.hashSum = hashSum; } } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/model/domain/sys/Api.java ================================================ package com.acimage.common.model.domain.sys; import com.acimage.common.global.enums.MatchRule; import com.acimage.common.global.enums.MyHttpMethod; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableLogic; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import org.springframework.http.HttpMethod; import javax.validation.constraints.NotNull; import javax.validation.constraints.Positive; import java.util.Date; @Data @NoArgsConstructor @AllArgsConstructor public class Api { public static final int PATH_MIN = 2; public static final int PATH_MAX = 200; public static final int NOTE_MAX = 100; public static final String PATH_PATTERN="(/([a-zA-Z0-9]+|(\\*){1,2}))+"; @TableId(type = IdType.AUTO) Integer id; String path; MyHttpMethod method; Integer permissionId; String note; boolean enable; Date createTime; Date updateTime; @TableLogic(delval = "1") boolean deleted; @TableField(exist = false) Permission permission; } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/model/domain/sys/Authorize.java ================================================ package com.acimage.common.model.domain.sys; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.fasterxml.jackson.annotation.JsonFormat; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.util.Date; @Data @NoArgsConstructor @AllArgsConstructor public class Authorize { @TableId(type = IdType.AUTO) Integer id; Integer roleId; Integer permissionId; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8") private Date createTime; public Authorize(Integer roleId, Integer permissionId) { this.roleId = roleId; this.permissionId = permissionId; } } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/model/domain/sys/Permission.java ================================================ package com.acimage.common.model.domain.sys; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.fasterxml.jackson.annotation.JsonFormat; import lombok.Data; import lombok.NoArgsConstructor; import java.util.Date; import java.util.List; @Data @NoArgsConstructor public class Permission { public static final int CODE_MAX=50; public static final int NOTE_MAX=20; public static final int LABEL_MAX=20; public static final String CODE_VALIDATION_MSG="权限码字数小于"+CODE_MAX; public static final String NOTE_VALIDATION_MSG="备注字数小于"+NOTE_MAX; public static final String LABEL_VALIDATION_MSG="标识字数小于"+LABEL_MAX; @TableId(type = IdType.AUTO) Integer id; Integer parentId; String code; String note; String label; boolean module; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8") private Date createTime; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8") private Date updateTime; @TableField(exist = false) List children; @TableField(exist = false) Permission parent; } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/model/domain/sys/Role.java ================================================ package com.acimage.common.model.domain.sys; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.fasterxml.jackson.annotation.JsonFormat; import lombok.Data; import lombok.NoArgsConstructor; import java.util.Date; @Data @NoArgsConstructor public class Role { public static final int ROLE_NAME_MAX=20; public static final int ROLE_NAME_MIN =2; public static final String ROLE_NAME_VALIDATION_MSG ="角色名在"+ ROLE_NAME_MIN +"-"+ROLE_NAME_MAX+"之间"; public static final int NOTE_MAX=20; public static final String NOTE_VALIDATION_MSG ="备注小于"+ROLE_NAME_MAX+"字"; @TableId(type = IdType.AUTO) Integer id; String roleName; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8") private Date createTime; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8") private Date updateTime; String note; } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/model/domain/sys/UserRole.java ================================================ package com.acimage.common.model.domain.sys; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.util.Date; @Data @NoArgsConstructor @AllArgsConstructor public class UserRole { @TableId(type = IdType.INPUT) private Long id; private Long userId; private Integer roleId; private Date createTime; } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/model/domain/user/CommentMsg.java ================================================ package com.acimage.common.model.domain.user; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import java.util.Date; @Data @NoArgsConstructor @AllArgsConstructor @Builder public class CommentMsg { @TableId(type = IdType.INPUT) Long commentId; String content; Long fromUserId; Long toUserId; Long topicId; String topicTitle; Date createTime; @TableField(exist = false) User user; } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/model/domain/user/User.java ================================================ package com.acimage.common.model.domain.user; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.util.List; @Data @NoArgsConstructor @AllArgsConstructor public class User { @TableId(type= IdType.INPUT) private Long id; private String username; private String photoUrl; @TableField(exist = false) private Integer starCount; @TableField(exist = false) private Integer topicCount; @TableField(exist = false) List roleIds; public User(Long id, String username) { this.id = id; this.username = username; } } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/model/domain/user/UserMsg.java ================================================ package com.acimage.common.model.domain.user; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.util.Date; import java.util.List; @Data @NoArgsConstructor @AllArgsConstructor public class UserMsg { @TableId(type= IdType.INPUT) private Long userId; private Integer commentMsgCount; private Integer starMsgCount; private Date readCommentTime; private Date readStarTime; } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/model/domain/user/UserPrivacy.java ================================================ package com.acimage.common.model.domain.user; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.fasterxml.jackson.annotation.JsonFormat; import lombok.Data; import lombok.NoArgsConstructor; import java.util.Date; @Data @NoArgsConstructor public class UserPrivacy { @TableId(type = IdType.INPUT) private Long userId; private String pwd; private String salt; private String email; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") private Date registerTime; public UserPrivacy(Long userId, String pwd, String salt, String email) { this.userId = userId; this.pwd = pwd; this.salt = salt; this.email = email; } } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/model/mq/dto/EsAddDto.java ================================================ package com.acimage.common.model.mq.dto; import lombok.*; @Getter @Setter @NoArgsConstructor public class EsAddDto extends ObjectWithClass { public EsAddDto(Object obj){ this.with(obj); } @Override public String toString() { return super.toString(); } } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/model/mq/dto/EsDeleteDto.java ================================================ package com.acimage.common.model.mq.dto; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor @AllArgsConstructor public class EsDeleteDto { String id; Class type; } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/model/mq/dto/EsUpdateByIdDto.java ================================================ package com.acimage.common.model.mq.dto; import lombok.*; import java.util.List; @Getter @Setter @NoArgsConstructor public class EsUpdateByIdDto extends ObjectWithClass { List columns; @Override public String toString() { return columns+super.toString(); } // List> columns; } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/model/mq/dto/EsUpdateByTermDto.java ================================================ package com.acimage.common.model.mq.dto; import lombok.Data; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import java.util.List; @Getter @Setter @NoArgsConstructor public class EsUpdateByTermDto extends ObjectWithClass{ String termColumn; List columns; @Override public String toString() { return "EsUpdateByTermDto{" + "termColumn='" + termColumn + '\'' + ", columns=" + columns + '}'+super.toString(); } } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/model/mq/dto/ImageIdWithUrl.java ================================================ package com.acimage.common.model.mq.dto; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @AllArgsConstructor @NoArgsConstructor public class ImageIdWithUrl { private Long imageId; private String url; } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/model/mq/dto/ObjectWithClass.java ================================================ package com.acimage.common.model.mq.dto; import com.acimage.common.utils.common.JacksonUtils; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; /** * 用于序列化时候传输 */ @Data @NoArgsConstructor @AllArgsConstructor public class ObjectWithClass { private String json; private Class type; public void with(Object obj){ this.setJson(JacksonUtils.writeValueAsString(obj)); this.setType(obj.getClass()); } /** * 千万别写成getObject,否则会导致序列化出错 */ public Object object(){ return JacksonUtils.convert(json,type); } } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/model/mq/dto/SyncImagesUpdateDto.java ================================================ package com.acimage.common.model.mq.dto; import com.acimage.common.global.enums.ServiceType; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import java.util.List; @Builder @Data @NoArgsConstructor @AllArgsConstructor public class SyncImagesUpdateDto { private Long topicId; /** * 话题内新增的图片链接 */ private List addImageUrls; /** * 话题内移除的图片链接 */ private List removeImageUrls; private ServiceType serviceType; } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/model/mq/dto/UserIdWithPhotoUrl.java ================================================ package com.acimage.common.model.mq.dto; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @AllArgsConstructor @NoArgsConstructor public class UserIdWithPhotoUrl { private Long userId; private String photoUrl; } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/model/mq/dto/UserIdWithUsername.java ================================================ package com.acimage.common.model.mq.dto; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @AllArgsConstructor @NoArgsConstructor public class UserIdWithUsername { private Long userId; private String username; } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/model/page/MyPage.java ================================================ package com.acimage.common.model.page; import com.baomidou.mybatisplus.core.metadata.IPage; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.util.List; @Data @AllArgsConstructor @NoArgsConstructor public class MyPage { int totalCount; List dataList; public static MyPage from(IPage page){ return new MyPage<>((int)page.getTotal(),page.getRecords()); } } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/redis/QueryRedisAdvice.java ================================================ package com.acimage.common.redis; import com.acimage.common.redis.annotation.KeyParam; import com.acimage.common.redis.annotation.QueryRedis; import com.acimage.common.redis.enums.DataType; import com.acimage.common.utils.common.*; import com.acimage.common.utils.redis.RedisUtils; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import javax.annotation.Nullable; import java.lang.reflect.Method; import java.util.Arrays; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @Aspect @Component @Order(100) @Slf4j public class QueryRedisAdvice { private static final String POINT_CUT_PATTERN = "@annotation(com.acimage.common.redis.annotation.QueryRedis)"; @Autowired RedisUtils redisUtils; @Pointcut(POINT_CUT_PATTERN) public void pointCut() { } @Around("pointCut()") public Object around(ProceedingJoinPoint joinPoint) throws Throwable { Method method = AopUtils.methodOf(joinPoint); QueryRedis queryRedis = AopUtils.annotationFrom(joinPoint, QueryRedis.class); Class returnType = method.getReturnType(); Class[] generics = ReflectUtils.genericsOfReturnType(method); //获取@QueryRedis的属性 String prefix = queryRedis.keyPrefix(); TimeUnit timeUnit = queryRedis.unit(); DataType dataType = queryRedis.dataType(); //获取被注解的形参和实参 List annotatedParams = AopUtils.paramAnnotationsFrom(joinPoint, KeyParam.class); List annotatedArgs = AopUtils.annotatedArgsFrom(joinPoint, KeyParam.class, String.class); //获取所有实参 List args = Arrays.stream(joinPoint.getArgs()).map(Object::toString).collect(Collectors.toList()); //合并出整体key String suffix = StringUtils.concatForNotNull(":", args); String overallKey = prefix + suffix; //根据注解和实参信息得到起作用的expire long expire = getActiveExpire(queryRedis.expire(), annotatedParams, annotatedArgs); if (expire <= 0) { return joinPoint.proceed(); } if (dataType == DataType.STRING) { return queryOrProceed(joinPoint, overallKey, expire, timeUnit, returnType, generics); } else if (dataType == DataType.HASH) { return queryOrProceedForHash(joinPoint, overallKey, expire, timeUnit, returnType); } else { throw new RuntimeException(this.getClass()+"到达不存在的分支"); } } /** * 查询redis,有则返回,无则调用原方法被设置到redis并返回,针对返回值是list的类型 */ private Object queryOrProceed(ProceedingJoinPoint joinPoint, String redisKey, long expire, TimeUnit timeUnit, Class returnType, @Nullable Class... generics) throws Throwable { //从redis获取 Object obj; Class[] newGenerics = (generics == null) ? new Class[]{} : generics; obj = redisUtils.getFromString(redisKey, returnType, newGenerics); if (obj != null) { return obj; } Object result = joinPoint.proceed(); //设置到redis if (result != null) { redisUtils.setObjectJson(redisKey, result, expire, timeUnit); } return result; } private Object queryOrProceedForHash(ProceedingJoinPoint joinPoint, String redisKey, long expire, TimeUnit timeUnit, Class returnType) throws Throwable { //从redis获取 Object obj; obj = redisUtils.getObjectForHash(redisKey, returnType); if (obj != null) { return obj; } Object result = joinPoint.proceed(); //设置到redis if (result != null) { redisUtils.setObjectForHash(redisKey, result,expire,timeUnit); } return result; } /** * 若当前实参值和@KeyParam中的specials匹配,则返回该参数注解的expire,否则返回default expire * @param annotatedArgs 被注解的实参值 */ private long getActiveExpire(long defaultExpire, List keyParams, List annotatedArgs) { assert keyParams.size() == annotatedArgs.size(); //先判断当前被注解实参值和它们的@KeyParam注解中的spValues参数是否符合 boolean isExpireChange = false; long expire = defaultExpire; for (int i = 0; i < keyParams.size(); i++) { String[] specials = keyParams.get(i).specials(); long[] expires = keyParams.get(i).expires(); //未设置specials则不用考虑 if (specials.length == 0) { continue; } Integer index = ArrayUtils.firstIndexOf(specials, annotatedArgs.get(i)); if (index == null) { //当前实参和@KeyParam注解中specials不匹配 return defaultExpire; } else { if (expires.length <= index) { log.error("@KeyParam注解使用错误:specials和expire长度不一致"); } else { if (!isExpireChange) { expire = expires[index]; isExpireChange = true; } } } } return expire; } } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/redis/RequestLimitAdvice.java ================================================ package com.acimage.common.redis; import com.acimage.common.global.context.UserContext; import com.acimage.common.redis.annotation.RequestLimit; import com.acimage.common.redis.enums.LimitTarget; import com.acimage.common.result.Result; import com.acimage.common.utils.common.*; import com.acimage.common.utils.redis.RedisUtils; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @Aspect @Component @Slf4j @Order(1) public class RequestLimitAdvice { private static final String POINT_CUT_PATTERN = "@annotation(com.acimage.common.redis.annotation.RequestLimit)"; private static final String STRINGKP_REQUEST_LIMIT = "acimage:request:limit:"; @Autowired RedisUtils redisUtils; @Pointcut(POINT_CUT_PATTERN) public void pointCut() { } @Around("pointCut()") public Object around(ProceedingJoinPoint joinPoint) throws Throwable { Method method = AopUtils.methodOf(joinPoint); RequestLimit requestLimit = AopUtils.annotationFrom(joinPoint, RequestLimit.class); String methodName = method.getName(); String className = method.getDeclaringClass().getName(); String predix = STRINGKP_REQUEST_LIMIT + String.format("%s:%s:", methodName, className); LimitTarget[] targets = requestLimit.targets(); long[] limitTimes = requestLimit.limitTimes(); long[] durations = requestLimit.durations(); long[] penaltyTimes = requestLimit.penaltyTimes(); TimeUnit timeUnit = requestLimit.unit(); int len = targets.length; if (limitTimes.length != len || durations.length != len || penaltyTimes.length != len) { throw new IllegalArgumentException("注解参数长度不一致"); } List keys = new ArrayList<>(); List limitList=new ArrayList<>(); List durationList=new ArrayList<>(); List penaltyList=new ArrayList<>(); List validTargetList=new ArrayList<>(); for (int i=0;i incrementResults = redisUtils.requestLimitScript(keys, limitList, durationList, penaltyList); for (int i = 0; i < incrementResults.size(); i++) { if (incrementResults.get(i) > limitList.get(i)) { switch (validTargetList.get(i)) { case IP: case USER: return Result.fail("请勿频繁操作"); case ALL: return Result.fail("系统繁忙,请稍后重试"); } } } return joinPoint.proceed(); } private List toSecondsList(long[] times, TimeUnit unit) { Long[] ts = new Long[times.length]; for (int i = 0; i < times.length; i++) { ts[i] = times[i]; } return Arrays.stream(ts).map(obj -> toSeconds(obj, unit)).collect(Collectors.toList()); } private long toSeconds(long time, TimeUnit timeUnit) { long res = time; switch (timeUnit) { case DAYS: res = res * 24; case HOURS: res = res * 60; case MINUTES: res = res * 60; case SECONDS: return res; } throw new IllegalArgumentException("只支持timeUnit为day,hour,minute,second"); } } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/redis/annotation/KeyParam.java ================================================ package com.acimage.common.redis.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 和@QueryRedis配合使用,被注解的参数作为redis键的后缀,可以有多个 * 但只能有一个KeyParam设置spValues和expire */ @Target({ElementType.FIELD,ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) public @interface KeyParam { /** * 后缀取这些值时,过期时间根据本身的expire设置 */ String[] specials() default {}; /** * 对应的过期时间 */ long[] expires() default {}; } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/redis/annotation/QueryRedis.java ================================================ package com.acimage.common.redis.annotation; import com.acimage.common.redis.enums.DataType; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.util.concurrent.TimeUnit; @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface QueryRedis { String keyPrefix() ; /** * expire<=0并且参数中没有@KeyParam设置了expire时,不会设置到redis, */ long expire() default 1L; TimeUnit unit() default TimeUnit.MINUTES; DataType dataType() default DataType.STRING; } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/redis/annotation/RequestLimit.java ================================================ package com.acimage.common.redis.annotation; import com.acimage.common.redis.enums.LimitTarget; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.util.concurrent.TimeUnit; @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface RequestLimit { long[] limitTimes(); long[] durations(); long[] penaltyTimes(); LimitTarget[] targets(); TimeUnit unit() default TimeUnit.SECONDS; } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/redis/enums/DataType.java ================================================ package com.acimage.common.redis.enums; public enum DataType { STRING, HASH; } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/redis/enums/LimitTarget.java ================================================ package com.acimage.common.redis.enums; public enum LimitTarget { USER, IP, ALL, } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/redis/utils/RedisAnnotationUtils.java ================================================ package com.acimage.common.redis.utils; import com.acimage.common.redis.annotation.KeyParam; import com.acimage.common.utils.common.AopUtils; import com.acimage.common.utils.redis.RedisUtils; import com.acimage.common.utils.common.StringUtils; import org.aspectj.lang.ProceedingJoinPoint; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.util.List; import java.util.concurrent.TimeUnit; @Component public class RedisAnnotationUtils { @Autowired RedisUtils redisUtils; /** * 查询redis,有则返回,无则调用原方法被设置到redis并返回 */ private Object queryOrProceed(ProceedingJoinPoint joinPoint, String redisKey, Class resultType, long expire, TimeUnit unit) throws Throwable { //查redis Object objectResult = redisUtils.getObjectFromString(redisKey, resultType); if (objectResult != null) { return objectResult; } Object result = joinPoint.proceed(); //设置到redis redisUtils.setObjectJson(redisKey, result, expire, unit); return result; } /** * 查询redis,有则返回,无则调用原方法被设置到redis并返回,针对返回值是list的类型 */ private Object queryOrProceedForList(ProceedingJoinPoint joinPoint, String redisKey, Class resultType, long expire, TimeUnit timeUnit) throws Throwable { //从redis获取 List objectList = redisUtils.getListFromString(redisKey, resultType); if (objectList != null) { return objectList; } Object result = joinPoint.proceed(); //设置到redis redisUtils.setObjectJson(redisKey,result,expire,timeUnit); return result; } public static String overallKey(String prefix, ProceedingJoinPoint joinPoint){ //获取所有KeyParam注解 List keyParams = AopUtils.paramAnnotationsFrom(joinPoint, KeyParam.class); //对应实参转String List args = AopUtils.annotatedArgsFrom(joinPoint, KeyParam.class, String.class); //合并出整体key String suffix = StringUtils.concatForNotNull(":", args); return (suffix == null) ? prefix : prefix + suffix; } } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/result/Code.java ================================================ package com.acimage.common.result; public class Code { public static final Integer SAVE_ERR=20010; public static final Integer OK=20000; public static final Integer ERR=20001; public static final Integer SYSTEM_ERR=50001; public static final Integer BUSINESS_ERR=50002; public static final Integer DATA_NOT_EXIST=20011; public static final Integer PARAM_INVALID =40001; public static final Integer TOKEN_ERR=40011; } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/result/Result.java ================================================ package com.acimage.common.result; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @AllArgsConstructor @NoArgsConstructor public class Result { private Integer code; private T data; private String msg; public static Result fail(String message){ return new Result(Code.ERR,null,message); } public static Result ok(){ return new Result(Code.OK,null,""); } public static Result ok(T data){ return new Result(Code.OK,data,""); } public static boolean isOk(Result result){ return Code.OK.equals(result.getCode()); } public Boolean isOk(){ return Code.OK.equals(code); } } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/service/TokenService.java ================================================ package com.acimage.common.service; public interface TokenService { Boolean hasRecorded(String token); String createAndRecordToken(long userId, String username, String photoUrl,int expireDays); void record(String token, int expireDays); void invalidate(String token); } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/service/impl/TokenServiceImpl.java ================================================ package com.acimage.common.service.impl; import com.acimage.common.global.consts.JwtConstants; import com.acimage.common.global.context.UserContext; import com.acimage.common.service.TokenService; import com.acimage.common.utils.JwtUtils; import com.acimage.common.utils.redis.RedisUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.concurrent.TimeUnit; @Service public class TokenServiceImpl implements TokenService { public static final String STRINGKP_TOKEN ="acimage:tokens:token:"; @Autowired RedisUtils redisUtils; @Override public String createAndRecordToken(long userId, String username, String photoUrl,int expireDays){ //生成token String token = JwtUtils.createToken(userId, username,photoUrl,expireDays); //记录token和ip的对应 record(token, expireDays); return token; } @Override public void record(String token,int expireDays){ redisUtils.setAsString(STRINGKP_TOKEN +token,Boolean.TRUE.toString(), expireDays, TimeUnit.DAYS); } @Override public Boolean hasRecorded(String token){ return redisUtils.getForString(STRINGKP_TOKEN +token)!=null; } @Override public void invalidate(String token) { redisUtils.delete(STRINGKP_TOKEN +token); } } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/utils/CookieUtils.java ================================================ package com.acimage.common.utils; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletResponse; public class CookieUtils { public static String getValueByName(Cookie[] cookies,String name){ if(cookies==null){ return null; } for(int i=0;i< cookies.length;i++){ if(cookies[i].getName().equals(name)){ return cookies[i].getValue(); } } return null; } public static Cookie createCookie(String key,String value){ Cookie cookie = new Cookie(key,value); cookie.setPath("/"); cookie.setMaxAge(-1); return cookie; } public static Cookie createCookie(String key,String value,boolean httpOnly){ Cookie cookie = new Cookie(key,value); cookie.setPath("/"); cookie.setMaxAge(-1); cookie.setHttpOnly(httpOnly); return cookie; } public static Cookie createCookie(String key,String value,String path,int expire){ Cookie cookie = new Cookie(key,value); cookie.setPath(path); cookie.setMaxAge(expire); return cookie; } } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/utils/EsUtils.java ================================================ package com.acimage.common.utils; import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.date.DatePattern; import cn.hutool.core.date.DateUtil; import com.acimage.common.model.Index.TopicIndex; import com.acimage.common.model.domain.community.Topic; import com.acimage.common.model.mq.dto.EsAddDto; import com.acimage.common.model.mq.dto.EsDeleteDto; import com.acimage.common.model.mq.dto.EsUpdateByIdDto; import com.acimage.common.model.mq.dto.EsUpdateByTermDto; import com.acimage.common.model.page.MyPage; import com.acimage.common.utils.common.BeanUtils; import com.acimage.common.utils.common.ReflectUtils; import lombok.extern.slf4j.Slf4j; import org.elasticsearch.index.query.MatchQueryBuilder; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.search.sort.FieldSortBuilder; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.data.annotation.Id; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.elasticsearch.core.*; import org.springframework.data.elasticsearch.core.document.Document; import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; import org.springframework.data.elasticsearch.core.query.*; import org.springframework.stereotype.Component; import reactor.util.annotation.Nullable; import java.util.*; import java.util.stream.Collectors; @Component @Slf4j @ConditionalOnClass(ElasticsearchRestTemplate.class) public class EsUtils { @Autowired ElasticsearchRestTemplate esTemplate; public void createIndexIfNotExist(Class entityType) { IndexCoordinates indexCoordinates = indexCoordinatesOf(entityType); IndexOperations indexOperations = esTemplate.indexOps(indexCoordinates); if (!indexOperations.exists()) { // 创建索引和映射 indexOperations.create(); indexOperations.refresh(); Document mapping = indexOperations.createMapping(entityType); indexOperations.refresh(); indexOperations.putMapping(mapping); indexOperations.refresh(); log.info("创建索引和映射关系成功 {}", entityType.getSimpleName()); } } public void updateById(EsUpdateByIdDto updateDto) { Object entity = updateDto.object(); if (entity == null) { return; } List columns = updateDto.getColumns(); if (CollectionUtil.isEmpty(columns)) { return; } //获取id Class indexClass = entity.getClass(); String id = Objects.requireNonNull(ReflectUtils.getAnnotatedFiled(entity, Id.class)).toString(); IndexCoordinates indexCoordinates = indexCoordinatesOf(indexClass); Document document = this.createDocument(entity, columns); UpdateQuery updateQuery = UpdateQuery.builder(id) .withDocument(document) .build(); esTemplate.update(updateQuery, indexCoordinates); } public void UpdateByTerm(EsUpdateByTermDto updateDto) { Object entity = updateDto.object(); if (entity == null) { return; } List columns = updateDto.getColumns(); if (CollectionUtil.isEmpty(columns)) { return; } //获取更新依据的term Class indexClass = entity.getClass(); String termColumn = updateDto.getTermColumn(); Object termValue = BeanUtil.getFieldValue(entity, termColumn); QueryBuilder queryBuilder = QueryBuilders.termQuery(termColumn, termValue); IndexCoordinates indexCoordinates = indexCoordinatesOf(indexClass); //建立脚本和参数 Map params = new HashMap<>(); StringBuilder script = new StringBuilder(); for (String column : columns) { Object value = BeanUtil.getFieldValue(entity, column); script.append(String.format("ctx._source.%s=params.%s;", column, column)); params.put(column, value); } Query query = new NativeSearchQueryBuilder() .withQuery(queryBuilder) .build(); UpdateQuery updateQuery = UpdateQuery.builder(query) .withParams(params) .withScript(script.toString()) .withScriptType(ScriptType.INLINE) .withAbortOnVersionConflict(false) .build(); ByQueryResponse byQueryResponse = esTemplate.updateByQuery(updateQuery, indexCoordinates); log.debug("更新了{}条", byQueryResponse.getUpdated()); } public void batchUpdateById(List entityList, List columns) { if (CollectionUtil.isEmpty(entityList)) { return; } Class clz = entityList.get(0).getClass(); List updateQueries = new ArrayList<>(); for (T entity : entityList) { String id = Objects.requireNonNull(ReflectUtils.getAnnotatedFiled(entity, Id.class)).toString(); Document document = this.createDocument(entity, columns); UpdateQuery updateQuery = UpdateQuery.builder(id) .withDocument(document) .build(); updateQueries.add(updateQuery); } esTemplate.bulkUpdate(updateQueries, clz); } public void save(EsAddDto esAddDto) { Object obj = esAddDto.object(); esTemplate.save(obj); } public void remove(EsDeleteDto esDeleteDto) { esTemplate.delete(esDeleteDto.getId(), esDeleteDto.getType()); } public MyPage termQuery(String column, Object value, Class indexClass, int pageNo, int pageSize, @Nullable FieldSortBuilder sortBuilder) { QueryBuilder queryBuilder = QueryBuilders.termQuery(column, value); Pageable pageable = PageRequest.of(pageNo - 1, pageSize); NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder() .withQuery(queryBuilder) .withPageable(pageable); if (sortBuilder != null) { nativeSearchQueryBuilder.withSort(sortBuilder); } SearchHits search = esTemplate.search(nativeSearchQueryBuilder.build(), indexClass); int totalCount = (int) search.getTotalHits(); List dateList = toList(search.getSearchHits()); return new MyPage<>(totalCount, dateList); } public List similarQuery(String id, Class index, List fields, int pageNo, int pageSize) { MoreLikeThisQuery moreLikeThisQuery = new MoreLikeThisQuery(); moreLikeThisQuery.setId(id); moreLikeThisQuery.addFields(fields.toArray(fields.toArray(new String[0]))); moreLikeThisQuery.setPageable(PageRequest.of(pageNo - 1, pageSize)); moreLikeThisQuery.setMinTermFreq(1); moreLikeThisQuery.setMinDocFreq(2); log.debug("{}", esTemplate.search(moreLikeThisQuery, index).getSearchHits()); return toList(esTemplate.search(moreLikeThisQuery, index).getSearchHits()); } public List matchQuery(Class index, String field, Object value, int pageNo, int pageSize, float score) { MatchQueryBuilder matchQuery = QueryBuilders.matchQuery(field, value); Pageable pageable = PageRequest.of(pageNo - 1, pageSize); NativeSearchQuery nativeSearchQuery = new NativeSearchQueryBuilder() .withQuery(matchQuery) .withPageable(pageable) .withMinScore(score) .build(); SearchHits search = esTemplate.search(nativeSearchQuery, index); log.debug("{}", search.getSearchHits()); return toList(search.getSearchHits()); } public MyPage queryBySort(Class indexClass, int pageNo, int pageSize, @Nullable FieldSortBuilder sortBuilder) { Pageable pageable = PageRequest.of(pageNo - 1, pageSize); NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder() .withPageable(pageable); if (sortBuilder != null) { nativeSearchQueryBuilder.withSort(sortBuilder); } SearchHits search = esTemplate.search(nativeSearchQueryBuilder.build(), indexClass); int totalCount = (int) search.getTotalHits(); List dateList = toList(search.getSearchHits()); return new MyPage<>(totalCount, dateList); } public IndexCoordinates indexCoordinatesOf(Class clz) { String indexName = clz.getAnnotation(org.springframework.data.elasticsearch.annotations.Document.class) .indexName(); return IndexCoordinates.of(indexName); } private List toList(List> searchHits) { return searchHits.stream() .map(SearchHit::getContent) .collect(Collectors.toList()); } private Document createDocument(Object entity, List columns) { Document document = Document.create(); //根据要更新的字段创建对应map for (String column : columns) { Object value = BeanUtil.getFieldValue(entity, column); document.put(column, value); } return document; } private String buildScript(Object entity, List columns) { StringBuilder script = new StringBuilder(); for (String column : columns) { Object value = BeanUtil.getFieldValue(entity, column); script.append(String.format("ctx.source.%s=params.%s;", column, column)); } return script.toString(); } } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/utils/ExceptionUtils.java ================================================ package com.acimage.common.utils; public class ExceptionUtils { public static void printIfDev(Throwable e){ if(SpringContextUtils.isDev()){ e.printStackTrace(); } } } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/utils/HtmlUtils.java ================================================ package com.acimage.common.utils; import com.acimage.common.utils.common.ListUtils; import java.util.ArrayList; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; public class HtmlUtils { private static final Pattern imageSrcPattern; static { String regex = "]*\\bsrc\\b\\s*=\\s*('|\")?(/[^'\"\n\r\f>]+(\\.jpg|\\.png\\.jpe|\\.jpeg|\\.webp))\\b[^>]*>"; imageSrcPattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE); } public static String html2Text(String strHtml) { String content = strHtml.replaceAll("]+>", ""); //剔出的标签 content = content.replaceAll("(\\s|\t|\r|\n)+", " ");//去除字符串中的空格,回车,换行符,制表符 content = content.replaceAll("(&[a-z]{2,6}+;)+", " ");//替换掉如 之类的符号 return content; } /** * 获取html中相对路径开头的图片,并对结果去重 */ public static List getInnerImageUrls(String html) { List imageSrcList = new ArrayList<>(); Matcher m = imageSrcPattern.matcher(html); String src; while (m.find()) { String quote = m.group(1); src = (quote == null || quote.trim().length() == 0) ? m.group(2).split("\\s+")[0] : m.group(2); imageSrcList.add(src); } return ListUtils.removeRepeat(imageSrcList); } } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/utils/IdGenerator.java ================================================ package com.acimage.common.utils; import cn.hutool.core.util.IdUtil; public class IdGenerator { public static long getSnowflakeNextId(){ return IdUtil.getSnowflakeNextId(); } } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/utils/ImageUtils.java ================================================ package com.acimage.common.utils; import com.acimage.common.global.exception.BusinessException; import com.acimage.common.global.consts.FileFormatConstants; import com.acimage.common.utils.common.FileUtils; import lombok.extern.slf4j.Slf4j; import net.coobird.thumbnailator.Thumbnails; import org.springframework.web.multipart.MultipartFile; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.Collections; @Slf4j public class ImageUtils { public static InputStream compressAsFixedWebpImage(MultipartFile multipartFile, int width, int height, int limitSize) { try { float qualify = 0.76f; BufferedImage bufferedImage = Thumbnails.fromInputStreams(Collections.singletonList(multipartFile.getInputStream())) .outputQuality(qualify) .forceSize(width, height) .outputFormat(FileFormatConstants.WEBP) .asBufferedImage(); try (InputStream is = ImageUtils.bufferedImage2InputStream(bufferedImage, FileFormatConstants.WEBP)) { if (is != null && is.available() > limitSize) { log.warn("the image size after compressing exceed {} size:{}", limitSize, is.available()); throw new BusinessException("图片压缩后仍然较大,请尝试其它图片"); } return is; } } catch (IOException e) { log.error(e.getMessage()); throw new BusinessException("图片数据错误"); } } public static InputStream compressAsWebpImage(MultipartFile multipartFile, int limitSize, int limitLength) { try { float qualify = 0.76f; BufferedImage image = ImageIO.read(multipartFile.getInputStream()); int width = image.getWidth(); int height = image.getHeight(); if (width > limitLength || height > limitLength) { if (width > height) { width = limitLength; //等比例缩放 height = (int) (1.0 * height * limitLength / width); } else { height = limitLength; width = (int) (1.0 * width * limitLength / height); } } BufferedImage bufferedImage = Thumbnails.fromInputStreams(Collections.singletonList(multipartFile.getInputStream())) .outputQuality(qualify) .size(width, height) .outputFormat(FileFormatConstants.WEBP) .asBufferedImage(); try (InputStream is = ImageUtils.bufferedImage2InputStream(bufferedImage, FileFormatConstants.WEBP)) { if (is != null && is.available() > limitSize) { log.warn("the image size after compressing exceed {} size:{}", limitSize, is.available()); throw new BusinessException("图片压缩后仍然较大,请尝试其它图片"); } return is; } } catch (IOException e) { log.error(e.getMessage()); throw new BusinessException("图片数据错误"); } } public static InputStream compressBak(MultipartFile multipartFile, int limitSize) { String format = FileUtils.formatOf(multipartFile); try { long size = multipartFile.getSize(); if (size < limitSize) { return multipartFile.getInputStream(); } // if (size < 240 * 1000) { // return multipartFile.getInputStream(); // } else if (limitSize * 1.0 / size > 0.8f) { // return multipartFile.getInputStream(); // } // Thumbnails.fromInputStreams(Collections.singletonList(multipartFile.getInputStream())); BufferedImage bufferedImage = ImageIO.read(multipartFile.getInputStream()); if (bufferedImage == null) { log.error("bufferedImage为空"); throw new BusinessException("图像为空"); } Thumbnails.Builder imageBuilder = Thumbnails.fromImages(Collections.singletonList(bufferedImage)); // BufferedImage bufferedImage = inputStreamBuilder.asBufferedImage(); int height = bufferedImage.getHeight(); int width = bufferedImage.getWidth(); //按边压缩比例,要开根号 double scale = Math.sqrt(limitSize * 1.0 / (width * height)); imageBuilder.scale(scale).outputQuality(1f); // imageBuilder.size(270, 260).outputQuality(1f); return ImageUtils.bufferedImage2InputStream(imageBuilder.asBufferedImage(), FileUtils.formatOf(multipartFile)); } catch (IOException e) { log.error(e.getMessage()); throw new RuntimeException(e); } } public static InputStream bufferedImage2InputStream(BufferedImage image, String format) { ByteArrayOutputStream os = new ByteArrayOutputStream(); try { ImageIO.write(image, format, os); return new ByteArrayInputStream(os.toByteArray()); } catch (IOException e) { log.error(e.getMessage()); } return null; } } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/utils/IpUtils.java ================================================ package com.acimage.common.utils; import cn.hutool.core.util.StrUtil; import com.acimage.common.global.consts.HeaderKeyConstants; import lombok.extern.slf4j.Slf4j; import org.springframework.http.server.reactive.ServerHttpRequest; import javax.servlet.http.HttpServletRequest; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.Objects; @Slf4j public class IpUtils { //获取请求客户端IP地址 public static String getClientIpAddr(HttpServletRequest request) { String ip = request.getHeader("x-forwarded-for"); if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("Proxy-Client-IP"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("WL-Proxy-Client-IP"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getRemoteAddr(); if ("127.0.0.1".equals(ip) || "0:0:0:0:0:0:0:1".equals(ip)) { //根据网卡取本机配置的IP InetAddress inet = null; try { inet = InetAddress.getLocalHost(); } catch (Exception e) { ExceptionUtils.printIfDev(e); } assert inet != null; ip = inet.getHostAddress(); } } // 多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割 if (ip != null && ip.length() > 15) { if (ip.indexOf(",") > 0) { ip = ip.substring(0, ip.indexOf(",")); } } return ip; } private final static String IP_UTILS_FLAG = ","; // 未知IP private final static String UNKNOWN = "unknown"; // 本地 IP private final static String LOCALHOST_IP = "0:0:0:0:0:0:0:1"; private final static String LOCALHOST_IP1 = "127.0.0.1"; public static String getUserIp(ServerHttpRequest request) { // 多次反向代理后会有多个ip值 的分割符 // 根据 HttpHeaders 获取 请求 IP地址 String ip = request.getHeaders().getFirst("X-Forwarded-For"); // if (StrUtil.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) { // ip = request.getHeaders().getFirst("x-forwarded-for"); // if (ip != null && ip.length() != 0 && !UNKNOWN.equalsIgnoreCase(ip)) { // // 多次反向代理后会有多个ip值,第一个ip才是真实ip // if (ip.contains(IP_UTILS_FLAG)) { // ip = ip.split(IP_UTILS_FLAG)[0]; // } // } // } if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) { ip = request.getHeaders().getFirst("Proxy-Client-IP"); } if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) { ip = request.getHeaders().getFirst("WL-Proxy-Client-IP"); } if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) { ip = request.getHeaders().getFirst("HTTP_CLIENT_IP"); } if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) { ip = request.getHeaders().getFirst("HTTP_X_FORWARDED_FOR"); } if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) { ip = request.getHeaders().getFirst("X-Real-IP"); } //兼容k8s集群获取ip if (StrUtil.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) { ip = Objects.requireNonNull(request.getRemoteAddress()).getAddress().getHostAddress(); if (LOCALHOST_IP1.equalsIgnoreCase(ip) || LOCALHOST_IP.equalsIgnoreCase(ip)) { //根据网卡取本机配置的IP InetAddress iNet = null; try { iNet = InetAddress.getLocalHost(); ip = iNet.getHostAddress(); } catch (UnknownHostException e) { log.error("getClientIp error:{}", e.getMessage()); } } } return ip; } public static String getIp(HttpServletRequest request) { String originUserIp = request.getHeader(HeaderKeyConstants.FEIGN_X_USER_IP); if (!StrUtil.isEmpty(originUserIp)) { return originUserIp; } else { return getClientIpAddr(request); } } } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/utils/JwtUtils.java ================================================ package com.acimage.common.utils; import cn.hutool.core.date.DateUtil; import cn.hutool.core.util.StrUtil; import com.acimage.common.global.consts.JwtConstants; import com.auth0.jwt.JWT; import com.auth0.jwt.JWTVerifier; import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.exceptions.JWTDecodeException; import com.auth0.jwt.exceptions.JWTVerificationException; import com.acimage.common.global.exception.NullTokenException; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; import java.util.Date; @Component public class JwtUtils { @Value("${jwt.secret-key}") public String secretKey; private static String SECRET_KEY; @PostConstruct private void init(){ SECRET_KEY=this.secretKey; } public static String createToken(long userId, String username, String photoUrl, int expireDays) { return JWT.create() .withIssuedAt(new Date()) //发行时间 .withExpiresAt(DateUtil.offsetDay(new Date(), expireDays)) //有效截止时间 .withClaim(JwtConstants.KEY_USER_ID, userId) //载荷,存储不敏感的用户信息 .withClaim(JwtConstants.KEY_USERNAME, username) .withClaim(JwtConstants.KEY_PHOTO_URL, photoUrl) .sign(Algorithm.HMAC256(SECRET_KEY)); //加密(摘要) } public static void verifyToken(String token) throws JWTVerificationException { if (StrUtil.isBlank(token)) { throw new NullTokenException("token is null!"); } JWTVerifier verifier = JWT.require(Algorithm.HMAC256(SECRET_KEY)).build(); verifier.verify(token); } public static Long getUserId(String token) throws JWTDecodeException { return JWT.decode(token).getClaim(JwtConstants.KEY_USER_ID).asLong(); } public static String getUsername(String token) throws JWTDecodeException { return JWT.decode(token).getClaim(JwtConstants.KEY_USERNAME).asString(); } public static String getPhotoUrl(String token) throws JWTDecodeException { return JWT.decode(token).getClaim(JwtConstants.KEY_PHOTO_URL).asString(); } public static Date getExpire(String token) throws JWTDecodeException { return JWT.decode(token).getExpiresAt(); } } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/utils/LambdaUtils.java ================================================ package com.acimage.common.utils; import cn.hutool.core.util.ReflectUtil; import com.baomidou.mybatisplus.core.toolkit.StringUtils; import com.baomidou.mybatisplus.core.toolkit.support.SFunction; import lombok.extern.slf4j.Slf4j; import java.lang.invoke.SerializedLambda; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; @Slf4j public class LambdaUtils { public static final String TOKEN_GET="get"; public static final String TOKEN_IS="is"; public static final String WRITE_REPLACE="writeReplace"; public static String underlineColumnNameOf(SFunction getOrIs) { return StringUtils.camelToUnderline(columnOf(getOrIs)); } public static String columnOf(SFunction getOrIs) { // 从function取出序列化方法 Method writeReplaceMethod = ReflectUtil.getMethodByName(getOrIs.getClass(), WRITE_REPLACE); // 从序列化方法取出序列化的lambda信息 // boolean isAccessible = writeReplaceMethod.isAccessible(); /** * 如果要通过isAccessible设置回去,一定要考虑并发问题 */ writeReplaceMethod.setAccessible(true); SerializedLambda serializedLambda; try { serializedLambda = (SerializedLambda) writeReplaceMethod.invoke(getOrIs); } catch (IllegalAccessException | InvocationTargetException e) { log.error(e.getMessage()); throw new RuntimeException(e); } //如果将访问属性设置回去,这里可能会出错 //writeReplaceMethod.setAccessible(isAccessible); // 从lambda信息取出method等 String methodName = serializedLambda.getImplMethodName(); String capitalizeFieldName; if (methodName.startsWith(TOKEN_IS)) { capitalizeFieldName = methodName.substring(TOKEN_IS.length()); } else if (methodName.startsWith(TOKEN_GET)) { capitalizeFieldName = methodName.substring(TOKEN_GET.length()); } else { throw new IllegalArgumentException(methodName + "前缀非get或is"); } return StringUtils.firstToLowerCase(capitalizeFieldName); } @SafeVarargs public static List columnsFrom(SFunction... getOrIsFunctions) { List columns=new ArrayList<>(); for(SFunction function:getOrIsFunctions){ columns.add(columnOf(function)); } return columns; } } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/utils/RsaUtils.java ================================================ package com.acimage.common.utils; import cn.hutool.crypto.asymmetric.KeyType; import cn.hutool.crypto.asymmetric.RSA; public class RsaUtils { private static final String privateKey; private static final String publicKey; static { RSA rsa = new RSA(); privateKey=rsa.getPrivateKeyBase64(); publicKey =rsa.getPublicKeyBase64(); } public static String decrypt(String privateKeyBase64,String encryptBase64){ RSA rsa=new RSA(privateKeyBase64,null); return rsa.decryptStr(encryptBase64, KeyType.PrivateKey); } public static String getPrivateKey(){ return privateKey; } public static String getPublicKey(){ return publicKey; } } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/utils/SensitiveWordUtils.java ================================================ package com.acimage.common.utils; import cn.hutool.core.io.file.FileReader; import cn.hutool.core.io.resource.ClassPathResource; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.stereotype.Component; import toolgood.words.StringSearch; import javax.annotation.PostConstruct; import java.io.*; import java.net.URL; import java.util.ArrayList; import java.util.List; @Component @ConditionalOnClass(StringSearch.class) public class SensitiveWordUtils { private static StringSearch search; String sensitiveWordFileName="sensitive_word.txt"; @PostConstruct private void init(){ // ClassPathResource classPathResource = new ClassPathResource(sensitiveWordFileName); InputStream inputStream = classPathResource.getStream(); List words = this.readLines(inputStream); search = new StringSearch(); search.SetKeywords(words); } private List readLines(InputStream inputStream){ InputStreamReader isr = new InputStreamReader(inputStream);//传入InputStream in 键盘录入对象 in List words=new ArrayList<>(); //为了提高效率,将字符串进行缓冲区技术高效操作.使用BufferedReader BufferedReader bufr = new BufferedReader(isr); String line = null; try { while ((line = bufr.readLine())!=null) { words.add(line); } } catch (IOException e) { throw new RuntimeException(e); }finally { try { bufr.close(); } catch (IOException e) { throw new RuntimeException(e); } } return words; } public static String filter(String str){ return search.Replace(str,'*'); } } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/utils/SpringContextUtils.java ================================================ package com.acimage.common.utils; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.stereotype.Component; @Component public class SpringContextUtils implements ApplicationContextAware { /** * spring的应用上下文 */ private static ApplicationContext applicationContext; /** * 初始化时将应用上下文设置进applicationContext */ @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { SpringContextUtils.applicationContext=applicationContext; } public static ApplicationContext getApplicationContext(){ return applicationContext; } public static Object getBean(String name) throws BeansException { return applicationContext.getBean(name); } public static T getBean(Class beanClass) throws BeansException { return applicationContext.getBean(beanClass); } /** * 获取spring.profiles.active * @return */ public static String getProfile(){ return getApplicationContext().getEnvironment().getActiveProfiles()[0]; } public static boolean isDev(){ String[] profiles=getApplicationContext().getEnvironment().getActiveProfiles(); for(String profile:profiles){ if(profile.startsWith("dev")){ return true; } } return false; } } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/utils/common/AopUtils.java ================================================ package com.acimage.common.utils.common; import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.convert.Convert; import com.acimage.common.utils.ExceptionUtils; import com.baomidou.mybatisplus.core.toolkit.StringUtils; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.reflect.MethodSignature; import javax.validation.constraints.NotNull; import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Parameter; import java.util.ArrayList; import java.util.List; @Slf4j public class AopUtils { public static Method methodOf(@NotNull JoinPoint joinPoint) { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method; try { method = joinPoint.getTarget() .getClass() .getMethod(signature.getMethod().getName(), signature.getMethod().getParameterTypes()); } catch (NoSuchMethodException e) { ExceptionUtils.printIfDev(e); throw new RuntimeException(e); } return method; } public static boolean hasParameters(@NotNull JoinPoint joinPoint) { Parameter[] parameters = methodOf(joinPoint).getParameters(); return parameters != null && parameters.length != 0; } public static T annotationFrom(JoinPoint joinPoint, Class annotation) { Method method = methodOf(joinPoint); return method.getAnnotation(annotation); } public static List paramAnnotationsFrom(JoinPoint joinPoint, Class annotation) { Parameter[] parameters = methodOf(joinPoint).getParameters(); List annotationList=new ArrayList<>(); for(int i=0;i V annotatedArgOrArgFieldOf(@NotNull JoinPoint joinPoint, Class annotation, Class targetType) { Parameter[] parameters = methodOf(joinPoint).getParameters(); Object[] args = joinPoint.getArgs(); Integer firstIndex = indexOfFirstAnnotatedParameter(parameters, annotation); if (firstIndex != null) { return Convert.convert(targetType, args[firstIndex]); } for(int i=0;i List annotatedArgsFrom(@NotNull JoinPoint joinPoint, Class annotation, Class targetType) { Parameter[] parameters = methodOf(joinPoint).getParameters(); Object[] args = joinPoint.getArgs(); List annotatedObjectList=new ArrayList<>(); for (int i = 0; i < parameters.length; i++) { if (parameters[i].isAnnotationPresent(annotation)) { annotatedObjectList.add(Convert.convert(targetType, args[i])); } } return annotatedObjectList; } private static Integer indexOfFirstAnnotatedParameter(Parameter[] parameters, Class annotation) { for (int i = 0; i < parameters.length; i++) { if (parameters[i].isAnnotationPresent(annotation)) { return i; } } return null; } } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/utils/common/ArrayUtils.java ================================================ package com.acimage.common.utils.common; import javax.validation.constraints.NotNull; import java.util.Arrays; import java.util.List; public class ArrayUtils { public static Integer firstIndexOf(String[] strs, String targetStr){ for(int i=0;i beanToFieldJsonMap(Object javaBean) { Map map = BeanUtil.beanToMap(javaBean, false, true); Map fieldJsonMap = new HashMap<>(); Set keys = map.keySet(); for (String key : keys) { Object obj = map.get(key); String json = JacksonUtils.writeValueAsString(obj); fieldJsonMap.put(key, json); } return fieldJsonMap; } public static T fieldJsonMapToBean(Map fieldJsonMap, Class clz) { T instance=ReflectUtil.newInstance(clz); List fieldName = ReflectUtils.getFieldNames(clz); Set keys = fieldJsonMap.keySet(); for (String key : keys) { if (fieldName.contains(key)) { String json = fieldJsonMap.get(key); //json转对象 Object obj = JacksonUtils.convert(json, ReflectUtil.getField(clz, key).getType()); //获取方法 String setMethodName = StringUtils.concatCapitalize(SET, key); Method setMethod = ReflectUtil.getMethodByName(clz, setMethodName); //调用方法,将属性set进去 ReflectUtil.invoke(instance, setMethod, obj); } } return instance; } public static T copyPropertiesTo(Object source,Class targetType){ T target=ReflectUtil.newInstance(targetType); BeanUtil.copyProperties(source,target,false); return target; } } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/utils/common/FileUtils.java ================================================ package com.acimage.common.utils.common; import cn.hutool.core.util.StrUtil; import cn.hutool.http.HttpUtil; import lombok.extern.slf4j.Slf4j; import org.springframework.web.multipart.MultipartFile; import javax.servlet.http.HttpServletResponse; import java.io.*; import java.net.URLEncoder; import java.nio.file.Files; import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; @Slf4j public class FileUtils { public static void packageFiles(File[] files, File zipFile) throws IOException { byte[] buffer = new byte[4096]; ZipOutputStream out = new ZipOutputStream(Files.newOutputStream(zipFile.toPath())); for (int i = 0; i < files.length; i++) { FileInputStream fis = new FileInputStream(files[i]); // FileInputStream fis = new FileInputStream(files[i].getPath()); out.putNextEntry(new ZipEntry(files[i].getName())); int len; // 读入需要下载的文件的内容,打包到zip文件 while ((len = fis.read(buffer)) > 0) { out.write(buffer, 0, len); } out.closeEntry(); fis.close(); } out.close(); } public static void downloadFileForClient(File file, HttpServletResponse response) throws IOException{ response.setCharacterEncoding("utf-8"); // response.setContentType("application/octet-stream"); // 以流的形式下载文件。 BufferedInputStream fis = new BufferedInputStream(Files.newInputStream(Paths.get(file.getPath()))); byte[] buffer = new byte[fis.available()]; fis.read(buffer); fis.close(); // 清空response response.reset(); OutputStream toClient = new BufferedOutputStream(response.getOutputStream()); response.setContentType("application/octet-stream"); response.setHeader("Content-Disposition", "attachment;filename=" + file.getName()); toClient.write(buffer); toClient.flush(); toClient.close(); } public static String formatOf(String fileName){ return StrUtil.subAfter(fileName,".",true); } public static String formatOf(MultipartFile multipartFile){ String fileName=multipartFile.getOriginalFilename(); return StrUtil.subAfter(fileName,".",true); } public static List formatsOf(MultipartFile[] multipartFiles){ if(multipartFiles==null){ return null; }else if(multipartFiles.length==0){ return new ArrayList<>(); }else{ List formatList=new ArrayList<>(); for (MultipartFile multipartFile : multipartFiles) { formatList.add(formatOf(multipartFile.getOriginalFilename())); } return formatList; } } } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/utils/common/JacksonUtils.java ================================================ package com.acimage.common.utils.common; import com.acimage.common.utils.ExceptionUtils; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; import java.util.List; @Slf4j public class JacksonUtils { private static final ObjectMapper mapper = new ObjectMapper(); static { //设置忽略空字段 mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); } public static String writeValueAsString(Object object) { String json = null; try { json = mapper.writeValueAsString(object); } catch (JsonProcessingException e) { ExceptionUtils.printIfDev(e); log.error("对象序列化成json错误 对象[{}] error:{}", object,e.getMessage()); throw new RuntimeException(e); } return json; } public static List getList(@Nullable String json, Class ofType) { if (json == null) { return null; } JavaType javaType = mapper.getTypeFactory().constructParametricType(List.class, ofType); try { return mapper.readValue(json, javaType); } catch (JsonProcessingException e) { log.error("数据反序列化异常 json:{} type:{} error:{}", json,javaType,e.getMessage()); throw new RuntimeException(e); } } public static T convert(@Nullable String json, Class targetType) { if (json == null) { return null; } try { return mapper.readValue(json, targetType); } catch (JsonProcessingException e) { log.error("数据反序列化异常 json:{} type:{} error:{}", json,targetType,e.getMessage()); throw new RuntimeException(e); } } } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/utils/common/ListUtils.java ================================================ package com.acimage.common.utils.common; import cn.hutool.core.convert.Convert; import cn.hutool.core.lang.Pair; import org.springframework.data.redis.core.ZSetOperations; import java.util.*; import java.util.function.Function; import java.util.stream.Collectors; public class ListUtils { public static List extract(Function attribute, List list) { if (list == null) { return null; } return list.stream().map(attribute).collect(Collectors.toList()); } public static List extractKeyFrom(List> pairList) { List keyList = new ArrayList<>(); for (Pair item : pairList) { keyList.add(item.getKey()); } return keyList; } public static List getListFrom(Set set) { if (set == null) { return null; } List list = new ArrayList<>(set.size()); return null; } public static List differenceSetOf(List list1, List list2) { if (list2 == null || list2.size() == 0) { return list1; } else if (list1 == null || list1.size() == 0) { return list1; } List list1Copy = new ArrayList<>(list1); List list2Copy = new ArrayList<>(list2); Comparator longComparator = Comparator.naturalOrder(); list1Copy.sort(longComparator); list2Copy.sort(longComparator); int ptr1 = 0, ptr2 = 0; List differenceList = new ArrayList<>(); while (ptr1 < list1Copy.size() && ptr2 < list2Copy.size()) { if (list1Copy.get(ptr1) < list2Copy.get(ptr2)) { differenceList.add(list1Copy.get(ptr1)); ptr1++; } else if (list1Copy.get(ptr1) > list2Copy.get(ptr2)) { ptr2++; } else if (list1Copy.get(ptr1).equals(list2Copy.get(ptr2))) { ptr1++; ptr2++; } } if (ptr2 == list2Copy.size()) { while (ptr1 < list1Copy.size()) { differenceList.add(list1Copy.get(ptr1)); ptr1++; } } return differenceList; } public static List differenceSetOfV2(List list1, List list2) { if (list2 == null || list2.size() == 0) { return list1; } else if (list1 == null || list1.size() == 0) { return list1; } List list1Copy = new ArrayList<>(list1); List list2Copy = new ArrayList<>(list2); Comparator stringComparator = Comparator.naturalOrder(); list1Copy.sort(stringComparator); list2Copy.sort(stringComparator); int ptr1 = 0, ptr2 = 0; List differenceList = new ArrayList<>(); while (ptr1 < list1Copy.size() && ptr2 < list2Copy.size()) { if (list1Copy.get(ptr1).compareTo(list2Copy.get(ptr2))<0 ) { differenceList.add(list1Copy.get(ptr1)); ptr1++; } else if (list1Copy.get(ptr1).compareTo(list2Copy.get(ptr2))>0 ) { ptr2++; } else if (list1Copy.get(ptr1).equals(list2Copy.get(ptr2))) { ptr1++; ptr2++; } } if (ptr2 == list2Copy.size()) { while (ptr1 < list1Copy.size()) { differenceList.add(list1Copy.get(ptr1)); ptr1++; } } return differenceList; } public static boolean isEmpty(List list) { return list == null || list.size() == 0; } public static List convertToLongList(List list) { List longList = new ArrayList<>(); for (String item : list) { longList.add(Long.parseLong(item)); } return longList; } public static List> getListInDescOrderFrom(Set> valueAnsScoreSet) { List> valueAnsScoreList = new ArrayList<>(); for (ZSetOperations.TypedTuple item : valueAnsScoreSet) { Pair pair = new Pair<>(Long.parseLong(item.getValue()), item.getScore().intValue()); valueAnsScoreList.add(pair); } return valueAnsScoreList; } public static List> convertToLongDoublePairFrom(List> pairList) { List> newList = new ArrayList<>(); for (Pair item : pairList) { newList.add(new Pair<>(Long.parseLong(item.getKey()), item.getValue())); } return newList; } public static List convert(List sourceList, Class targetType) { List resultList = new ArrayList<>(); if (sourceList == null) { return null; } else if (sourceList.size() == 0) { return new ArrayList<>(); } for (T item : sourceList) { resultList.add(Convert.convert(targetType, item)); } return resultList; } public static List removeRepeat(List list){ Set set=new HashSet<>(list); return new ArrayList<>(set); } } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/utils/common/PageUtils.java ================================================ package com.acimage.common.utils.common; public class PageUtils { public static int startIndexOf(int pageNo, int pageSize) { return (pageNo - 1) * pageSize; } public static int endIndexOf(int pageNo, int pageSize) { return pageNo * pageSize - 1; } } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/utils/common/PairUtils.java ================================================ package com.acimage.common.utils.common; import cn.hutool.core.lang.Pair; import javax.validation.constraints.NotNull; import java.util.ArrayList; import java.util.List; public class PairUtils { public static List> combine(@NotNull List list1, @NotNull List list2) { if (list1 == null) { throw new IllegalArgumentException("参数1为空"); }else if (list2 == null) { throw new IllegalArgumentException("参数2为空"); }else if (list1.size() != list2.size()) { throw new IllegalArgumentException(String.format("参数1长度[%s] 参数2长度[%s] 参数长度不一致!", list1.size(), list2.size())); }else if (list1.size() == 0) { return new ArrayList<>(); } int len = list1.size(); List> pairList = new ArrayList<>(len); for (int i = 0; i < len; i++) { pairList.add(new Pair<>(list1.get(i), list2.get(i))); } return pairList; } } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/utils/common/ReflectUtils.java ================================================ package com.acimage.common.utils.common; import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.convert.Convert; import com.baomidou.mybatisplus.core.toolkit.StringUtils; import lombok.extern.slf4j.Slf4j; import java.lang.annotation.Annotation; import java.lang.reflect.*; import java.util.ArrayList; import java.util.List; @Slf4j public class ReflectUtils { public static Class[] genericsOfReturnType(Method method) throws ClassNotFoundException { Class[] generics = new Class[]{}; Type genericReturnType = method.getGenericReturnType(); if (genericReturnType instanceof ParameterizedType ) { ParameterizedType parameterizedType=(ParameterizedType) genericReturnType; Type[] types = parameterizedType.getActualTypeArguments(); generics = new Class[types.length]; for (int i = 0; i < types.length; i++) { if (types[i] instanceof WildcardType) { generics[i] = Object.class; } else { generics[i] = Class.forName(types[i].getTypeName()); } } } return generics; } public static List getFieldNames(Class clz){ Field[] fields=clz.getDeclaredFields(); List fileNameList=new ArrayList<>(); for(Field field:fields){ fileNameList.add(field.getName()); } return fileNameList; } public static Object getAnnotatedFiled(Object obj,Class annotation){ Field field = ReflectUtils.firstAnnotatedField(obj.getClass(), annotation); if (field == null) { return null; } String fieldName = field.getName(); return BeanUtil.getFieldValue(obj,fieldName); } public static Field firstAnnotatedField(Class objectClass, Class annotation) { Field[] fields = objectClass.getDeclaredFields(); for (Field field : fields) { boolean isAnnotated = field.isAnnotationPresent(annotation); if (isAnnotated) { return field; } } return null; } } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/utils/common/StringUtils.java ================================================ package com.acimage.common.utils.common; import cn.hutool.core.collection.CollectionUtil; import javax.annotation.Nullable; import javax.validation.constraints.NotNull; import java.util.List; public class StringUtils { public static String concatForNotNull(@Nullable String separator, @Nullable List stringList) { if (CollectionUtil.isEmpty(stringList)) { return ""; } String newSeparator = separator == null ? "" : separator; //拼接 StringBuilder stringBuilder = new StringBuilder(); for (int i = 0; i < stringList.size(); i++) { stringBuilder.append(stringList.get(i)); if (i != stringList.size() - 1) { stringBuilder.append(newSeparator); } } return stringBuilder.toString(); } } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/utils/minio/MinioProperties.java ================================================ package com.acimage.common.utils.minio; import io.minio.MinioClient; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration @ConditionalOnClass(MinioClient.class) @ConfigurationProperties(prefix = "minio") public class MinioProperties { private String accessKey; private String secretKey; private String bucket; private String endpoint; public void setAccessKey(String accessKey) { this.accessKey = accessKey; } public String getBucket() { return bucket; } public String getEndpoint() { return endpoint; } public void setSecretKey(String secretKey) { this.secretKey = secretKey; } public void setBucket(String bucket) { this.bucket = bucket; } public void setEndpoint(String endpoint) { this.endpoint = endpoint; } @Bean public MinioClient minioClient(){ return MinioClient.builder() .endpoint(endpoint) .credentials(accessKey, secretKey) .build(); } } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/utils/minio/MinioUtils.java ================================================ package com.acimage.common.utils.minio; import cn.hutool.http.HttpUtil; import com.acimage.common.utils.ExceptionUtils; import io.minio.*; import io.minio.messages.DeleteObject; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.stereotype.Component; import org.springframework.web.multipart.MultipartFile; import javax.annotation.Nullable; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.List; @Slf4j @Component @ConditionalOnClass(MinioClient.class) public class MinioUtils { @Autowired MinioClient minioClient; @Autowired MinioProperties minioProperties; public String upload(MultipartFile multipartFile, String url) { InputStream in = null; try { in = multipartFile.getInputStream(); minioClient.putObject(PutObjectArgs.builder() .bucket(minioProperties.getBucket()) .object(url) .stream(in, in.available(), -1) .contentType(multipartFile.getContentType()) .build() ); } catch (Exception e) { ExceptionUtils.printIfDev(e); log.error(e.getMessage()); } finally { if (in != null) { try { in.close(); } catch (IOException e) { log.error(e.getMessage()); ExceptionUtils.printIfDev(e); } } } return String.format("/%s/%s", minioProperties.getBucket(), url); } public String upload(InputStream is, String url, String contentType) { try { minioClient.putObject(PutObjectArgs.builder() .bucket(minioProperties.getBucket()) .object(url) .contentType(contentType) .stream(is, is.available(), -1) .build() ); } catch (Exception e) { log.error(e.getMessage()); } finally { if (is != null) { try { is.close(); } catch (IOException e) { log.error(e.getMessage()); ExceptionUtils.printIfDev(e); } } } return String.format("/%s/%s", minioProperties.getBucket(), url); } public void downloadTo(String url, String destSrc) { try { String slashBucket = String.format("/%s", minioProperties.getBucket()); boolean isInnerUrl = url.startsWith(slashBucket); //不是站内图片则不下载 if (!isInnerUrl) { return; } String innerUrl = url.substring(slashBucket.length()); minioClient.downloadObject( DownloadObjectArgs.builder() .bucket(minioProperties.getBucket()) .object(innerUrl) .filename(destSrc) .build()); } catch (Exception e) { log.error(e.getMessage()); throw new RuntimeException(e); } } public void download(String url, String toPath) { // String encodedUrl = null; // try { // encodedUrl = URLEncoder.encode(url, "utf-8").replace("+", "%20"); // } catch (UnsupportedEncodingException e) { // e.printStackTrace(); // log.error("url编码失败 error:{}", e.getLocalizedMessage()); // } String publicUrl; if (url.startsWith("/")) { publicUrl = minioProperties.getEndpoint() + "/" + url; } else { publicUrl = minioProperties.getEndpoint() + url; } HttpUtil.downloadFile(publicUrl, new File(toPath));//下载 } public void deleteFile(String totalUrl) { String slashBucket = String.format("/%s", minioProperties.getBucket()); boolean isInnerUrl = totalUrl.startsWith(slashBucket); if (!isInnerUrl) { return; } String innerUrl = totalUrl.substring(slashBucket.length()); try { minioClient.removeObject(RemoveObjectArgs.builder() .bucket(minioProperties.getBucket()) .object(innerUrl) .build() ); } catch (Exception e) { log.error(e.getMessage()); ExceptionUtils.printIfDev(e); throw new RuntimeException(e); } } public void deleteFiles(List urls) { try { List deleteObjectList = new ArrayList<>(); for (String url : urls) { deleteObjectList.add(new DeleteObject(url)); } minioClient.removeObjects(RemoveObjectsArgs.builder() .bucket(minioProperties.getBucket()) .objects(deleteObjectList) .build()); } catch (Exception e) { log.error(e.getMessage()); ExceptionUtils.printIfDev(e); throw new RuntimeException(e); } } public String generateBaseUrl(@Nullable String prefix, Date uploadTime, String suffix) { String formatPattern = "yyyy/MM/dd"; String newPrefix = prefix == null ? "" : prefix + "/"; SimpleDateFormat formatter = new SimpleDateFormat(formatPattern); return newPrefix + formatter.format(uploadTime) + "/" + suffix; } } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/utils/redis/RedisLuaUtils.java ================================================ package com.acimage.common.utils.redis; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.ClassPathResource; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.script.DefaultRedisScript; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; import java.util.Arrays; import java.util.Collections; import java.util.List; @Component @Slf4j public class RedisLuaUtils { @Autowired private StringRedisTemplate stringRedisTemplate; private static final ObjectMapper mapper = new ObjectMapper(); private final static DefaultRedisScript incrementIfPresent = new DefaultRedisScript<>(); private final static DefaultRedisScript ifpForZSet = new DefaultRedisScript<>(); private final static DefaultRedisScript getAndCombineAndDelete = new DefaultRedisScript<>(); private final static DefaultRedisScript ifpForFieldKey = new DefaultRedisScript<>(); private final static DefaultRedisScript requestLimit = new DefaultRedisScript<>(); private final static DefaultRedisScript sfpForFieldKey = new DefaultRedisScript<>(); @PostConstruct public void init() { //设置忽略空字段 mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); // 指定脚本文件 incrementIfPresent.setLocation(new ClassPathResource("lua/incrementIfPresent.lua")); // 指定返回值 incrementIfPresent.setResultType(Long.class); getAndCombineAndDelete.setLocation(new ClassPathResource("lua/getAndCombineAndDelete.lua")); getAndCombineAndDelete.setResultType(String.class); ifpForZSet.setLocation(new ClassPathResource("lua/incrementIfPresentForZSet.lua")); ifpForZSet.setResultType(Long.class); ifpForFieldKey.setLocation(new ClassPathResource("lua/incrementIfPresentForFieldKey.lua")); ifpForFieldKey.setResultType(Long.class); requestLimit.setLocation(new ClassPathResource("lua/requestLimit.lua")); requestLimit.setResultType(List.class); sfpForFieldKey.setLocation(new ClassPathResource("lua/setIfPresentForFieldKey.lua")); sfpForFieldKey.setResultType(Boolean.class); } public Long incrementIfPresent(String key, long increment) { return stringRedisTemplate.opsForValue().getOperations() .execute(incrementIfPresent, Collections.singletonList(key), Long.toString(increment)); } /** * 如果keyForBase存在,则将redis中keyForIncrement的值增加到keyForBase中 * 否则获取并删除keyForIncrement * @return keyForIncrement对应的值 */ public Long getAndCombineAndDelete(String keyForIncrement, String fieldKeyForBase, String fieldKey) { String result = stringRedisTemplate.opsForValue().getOperations() .execute(getAndCombineAndDelete, Arrays.asList(keyForIncrement, fieldKeyForBase), fieldKey); if (result == null) { return null; } return Long.parseLong(result); } public Long incrementIfPresentForZSet(String key, String value, long increment) { return stringRedisTemplate.opsForValue().getOperations() .execute(ifpForZSet, Collections.singletonList(key), value, Long.toString(increment)); } public Long incrementIfPresentForFieldKey(String key, String filedKey, long increment) { return stringRedisTemplate.opsForValue().getOperations() .execute(ifpForFieldKey, Collections.singletonList(key), filedKey, Long.toString(increment)); } public List requestLimitScript(List keys, List limitTimes, List expireSeconds, List penaltyTimes) { int len = 3 * keys.size(); Object[] args = new Object[len]; int i = 0; for (Long limitTime : limitTimes) { args[i] = limitTime.toString(); i++; } for (Long expireSecond : expireSeconds) { args[i] = expireSecond.toString(); i++; } for (Long penaltyTime : penaltyTimes) { args[i] = penaltyTime.toString(); i++; } return stringRedisTemplate.execute(requestLimit, keys, args); } public Boolean setIfPresentForFieldKey(String key, String filedKey, String value) { return stringRedisTemplate.execute(sfpForFieldKey, Collections.singletonList(key), filedKey, value); } } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/utils/redis/RedisUtils.java ================================================ package com.acimage.common.utils.redis; import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.convert.Convert; import cn.hutool.core.lang.Pair; import com.acimage.common.utils.ExceptionUtils; import com.acimage.common.utils.SpringContextUtils; import com.acimage.common.utils.common.BeanUtils; import com.acimage.common.utils.common.JacksonUtils; import com.acimage.common.utils.common.ListUtils; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.ObjectMapper; import com.acimage.common.global.exception.BusinessException; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.ZSetOperations; import org.springframework.stereotype.Component; import javax.validation.constraints.NotNull; import java.util.*; import java.util.concurrent.TimeUnit; @Component @Slf4j public class RedisUtils { private static final ObjectMapper mapper = new ObjectMapper(); static { //设置忽略空字段 mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); } @Autowired private StringRedisTemplate stringRedisTemplate; @Autowired private RedisLuaUtils redisLuaUtils; @Deprecated public void setListAsString(String key, List objectList, long timeout, TimeUnit unit) { String json = JacksonUtils.writeValueAsString(objectList); stringRedisTemplate.opsForValue().set(key, json, timeout, unit); } public List getListFromString(String key, Class type) { String json = stringRedisTemplate.opsForValue().get(key); return JacksonUtils.getList(json, type); } public Object getFromString(String key, Class parameterized, Class... parametrizedTypes) { JavaType javaType = mapper.getTypeFactory().constructParametricType(parameterized, parametrizedTypes); String json = stringRedisTemplate.opsForValue().get(key); if (json == null) { return null; } Object result; try { //错误写法:result=mapper.readValue(json,new TypeReference>() { }); result = mapper.readValue(json, javaType); } catch (JsonProcessingException e) { ExceptionUtils.printIfDev(e); log.error("redis数据反序列化异常,key:{} json:{}", key, json); throw new BusinessException("服务器出错了~~请稍后重试(>m<)"); } return result; } public void setObjectJson(String key, @NotNull Object obj, long timeout, TimeUnit unit) { if (obj == null) { return; } String json = JacksonUtils.writeValueAsString(obj); stringRedisTemplate.opsForValue().set(key, json, timeout, unit); } public void setObjectJson(String key, @NotNull Object obj) { if (obj == null) { return; } String json = JacksonUtils.writeValueAsString(obj); stringRedisTemplate.opsForValue().set(key, json); } public T getObjectFromString(String key, Class targetType) { String json = stringRedisTemplate.opsForValue().get(key); return JacksonUtils.convert(json, targetType); } /** * @param targetType 仅支持Long、Double、Integer、String、Date,使用Date时set进去的数据需要是毫秒数 */ public T getForString(String key, Class targetType) { String str = stringRedisTemplate.opsForValue().get(key); if (str == null) { return null; } if (targetType == Date.class) { Long millis = Long.parseLong(str); return Convert.convert(targetType, millis); } return Convert.convert(targetType, str); } public String getForString(String key) { return stringRedisTemplate.opsForValue().get(key); } public List multiGetForString(List keys) { return stringRedisTemplate.opsForValue().multiGet(keys); } public void setAsString(String key, Object value) { stringRedisTemplate.opsForValue().set(key, value.toString()); } public void setAsString(String key, String value, long timeout, TimeUnit timeUnit) { stringRedisTemplate.opsForValue().set(key, value, timeout, timeUnit); } public Long increment(String key, long increment) { return stringRedisTemplate.opsForValue().increment(key, increment); } /** * redis6.2版本之后才支持 * * @param key */ public String getAndDeleteForString(String key) { return stringRedisTemplate.opsForValue().getAndDelete(key); } public String getAndExpire(String key, long timeout, TimeUnit timeUnit) { return stringRedisTemplate.opsForValue().getAndExpire(key, timeout, timeUnit); } public void setIfPresent(String key, String value) { stringRedisTemplate.opsForValue().setIfPresent(key, value); } public void setIfPresent(String key, String value, long timeout, TimeUnit timeUnit) { stringRedisTemplate.opsForValue().setIfPresent(key, value, timeout, timeUnit); } public Long incrementIfPresent(String key, long increment) { return redisLuaUtils.incrementIfPresent(key, increment); } public Boolean setIfPresentForFieldKey(String key, String filedKey, String value) { return redisLuaUtils.setIfPresentForFieldKey(key, filedKey, value); } public Long getAndCombineAndDelete(String keyForIncrement, String hashKeyForBase, String field) { return redisLuaUtils.getAndCombineAndDelete(keyForIncrement, hashKeyForBase, field); } public void setObjectForHash(String key, Object javaBean, long timeOut, TimeUnit timeUnit) { if (javaBean == null) { return; } Map fieldJsonMap = BeanUtils.beanToFieldJsonMap(javaBean); stringRedisTemplate.opsForHash().putAll(key, fieldJsonMap); stringRedisTemplate.expire(key, timeOut, timeUnit); } public T getObjectForHash(String key, Class targetType) { Map fieldJsonMap = stringRedisTemplate.opsForHash().entries(key); if (fieldJsonMap.size() == 0) { return null; } Map fieldJsonStringMap = new HashMap<>(); for (Object hashKey : fieldJsonMap.keySet()) { fieldJsonStringMap.put(hashKey.toString(), fieldJsonMap.get(hashKey).toString()); } return BeanUtils.fieldJsonMapToBean(fieldJsonStringMap, targetType); } public Boolean delete(String key) { return stringRedisTemplate.delete(key); } public Long delete(List key) { return stringRedisTemplate.delete(key); } public Boolean expire(String key, long timeout, TimeUnit timeUnit) { return stringRedisTemplate.expire(key, timeout, timeUnit); } public Long getExpire(String key, TimeUnit timeUnit) { return stringRedisTemplate.getExpire(key, timeUnit); } public Long addForHyperLogLog(String key, String... values) { return stringRedisTemplate.opsForHyperLogLog().add(key, values); } public void deleteForHyperLogLog(String key) { stringRedisTemplate.opsForHyperLogLog().delete(key); } public Long sizeForHyperLogLog(String key) { return stringRedisTemplate.opsForHyperLogLog().size(key); } public Long addForSet(String key, Object value) { return stringRedisTemplate.opsForSet().add(key, value.toString()); } public Boolean isMemberForSet(String key, String value) { return stringRedisTemplate.opsForSet().isMember(key, value); } public Long removeForSet(String key, String... values) { return stringRedisTemplate.opsForSet().remove(key, values); } public Long removeForSet(String key, List valueList) { if (CollectionUtil.isEmpty(valueList)) { return 0L; } Object[] values = ListUtils.convert(valueList, String.class).toArray(new String[valueList.size()]); return stringRedisTemplate.opsForSet().remove(key, values); } public List membersForSet(String key, Class targetType) { Set members = stringRedisTemplate.opsForSet().members(key); if (members == null) { return null; } List result = new ArrayList<>(); for (String member : members) { result.add(Convert.convert(targetType, member)); } return result; } public Boolean addForZSet(String key, String value, double score) { return stringRedisTemplate.opsForZSet().add(key, value, score); } public Double scoreForZSet(String key, String value) { return stringRedisTemplate.opsForZSet().score(key, value); } public Long sizeForZSet(String key) { return stringRedisTemplate.opsForZSet().size(key); } public Long removeForZSet(String key, Object toStringValue) { return stringRedisTemplate.opsForZSet().remove(key, toStringValue.toString()); } public List randomMembersForZSet(String key, int count) { if (SpringContextUtils.isDev()) { //randomMembers 在redis 6.2之后才有,我开发环境redis是windows 3.x版本,先自己实现一个 Long sizeLong = stringRedisTemplate.opsForZSet().size(key); if (sizeLong == null) { return new ArrayList<>(); } else if (sizeLong <= 0) { return new ArrayList<>(); } int size = sizeLong.intValue(); Random random = new Random(System.currentTimeMillis()); List resultList = new ArrayList<>(); for (int i = 0; i < count; i++) { int index = random.nextInt(size); Set set = stringRedisTemplate.opsForZSet().reverseRange(key, index, index); String idString = null; if (!CollectionUtil.isEmpty(set)) { for (String item : set) { idString = item; } } if (idString != null) { resultList.add(idString); } } return resultList; } else { return stringRedisTemplate.opsForZSet().randomMembers(key, count); } } public Long incrementIfPresentForZSet(String key, String value, long increment) { return redisLuaUtils.incrementIfPresentForZSet(key, value, increment); } public Set reverseRangeForZSet(String key, int start, int end) { return stringRedisTemplate.opsForZSet().reverseRange(key, start, end); } public Long incrementIfPresentForFieldKey(String key, String hashKey, long increment) { return redisLuaUtils.incrementIfPresentForFieldKey(key, hashKey, increment); } public List> reverseRangeWithScoreForZSet(String key, int start, int end) { Set> valueAnsScoreSet = stringRedisTemplate.opsForZSet().reverseRangeWithScores(key, start, end); if (valueAnsScoreSet == null) { return new ArrayList<>(); } List> valueAndScoreList = new ArrayList<>(); for (ZSetOperations.TypedTuple item : valueAnsScoreSet) { Pair pair = new Pair<>(item.getValue(), item.getScore()); valueAndScoreList.add(pair); } valueAndScoreList.sort((next, current) -> current.getValue().compareTo(next.getValue())); return valueAndScoreList; } public List> reverseRangeWithScoreForZSet(String key, int start, int end, Class valueType, Class scoreType) { Set> valueAnsScoreSet = stringRedisTemplate.opsForZSet().reverseRangeWithScores(key, start, end); if (valueAnsScoreSet == null) { return new ArrayList<>(); } List> valueAndScoreList = new ArrayList<>(); for (ZSetOperations.TypedTuple item : valueAnsScoreSet) { T value = Convert.convert(valueType, item.getValue()); Object newScore = item.getValue(); if (scoreType == Date.class) { assert newScore != null; newScore = Long.parseLong(newScore.toString()); } V score = Convert.convert(scoreType, newScore); Pair pair = new Pair<>(value, score); valueAndScoreList.add(pair); } return valueAndScoreList; } public List requestLimitScript(List keys, List limitTimes, List expireSeconds, List penaltyTimes) { return redisLuaUtils.requestLimitScript(keys, limitTimes, expireSeconds, penaltyTimes); } } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/web/exceptionhandler/ArgumentValidateExceptionHandler.java ================================================ package com.acimage.common.web.exceptionhandler; import com.acimage.common.result.Result; import com.acimage.common.global.context.UserContext; import com.acimage.common.utils.ExceptionUtils; import com.acimage.common.utils.SpringContextUtils; import lombok.extern.slf4j.Slf4j; import org.springframework.validation.BindException; import org.springframework.validation.FieldError; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import javax.validation.ConstraintViolation; import javax.validation.ConstraintViolationException; import java.util.List; import java.util.Set; @Slf4j @RestControllerAdvice public class ArgumentValidateExceptionHandler { /** * 单参数校验失败 */ @ExceptionHandler(value = {ConstraintViolationException.class}) public Result doConstraintViolationException(ConstraintViolationException ex) { ExceptionUtils.printIfDev(ex); log.warn(ex.getMessage()); //获取报错信息 Set> violations = ex.getConstraintViolations(); StringBuilder message = new StringBuilder(); int i = 0; for (ConstraintViolation violation : violations) { message.append(violation.getMessage()); i++; if (i < violations.size() - 1) { message.append(";"); } } //记录日志 logWarnMessage(ex, message.toString()); return Result.fail(message.toString()); } @ExceptionHandler(value = {MethodArgumentNotValidException.class}) public Result doMethodArgumentNotValidException(MethodArgumentNotValidException ex) { ExceptionUtils.printIfDev(ex); //获取报错信息 List fieldErrors = ex.getBindingResult().getFieldErrors(); StringBuilder logMessage = new StringBuilder(); StringBuilder userMessage = new StringBuilder(); for (int i = 0; i < fieldErrors.size(); i++) { FieldError fieldError = fieldErrors.get(i); logMessage.append(String.format("参数:%s 值:%s 错误:%s", fieldError.getField(), fieldError.getRejectedValue(), fieldError.getDefaultMessage())); userMessage.append(fieldError.getDefaultMessage()); if (i < fieldErrors.size() - 1) { logMessage.append(";"); userMessage.append(";"); } } //记录日志 logWarnMessage(ex, logMessage.toString()); return Result.fail(userMessage.toString()); } @ExceptionHandler(value = {BindException.class}) public Result doBindException(BindException ex) { ExceptionUtils.printIfDev(ex); //获取报错信息 List fieldErrors = ex.getBindingResult().getFieldErrors(); StringBuilder logMessage = new StringBuilder(); StringBuilder userMessage = new StringBuilder(); for (int i = 0; i < fieldErrors.size(); i++) { FieldError fieldError = fieldErrors.get(i); logMessage.append(String.format("参数:%s 值:%s 错误:%s", fieldError.getField(), fieldError.getRejectedValue(), fieldError.getDefaultMessage())); userMessage.append(fieldError.getDefaultMessage()); if (i < fieldErrors.size() - 1) { logMessage.append(";"); userMessage.append(";"); } } //记录日志 logWarnMessage(ex, logMessage.toString()); return Result.fail(userMessage.toString()); } private void logWarnMessage(Exception ex, String message) { String exceptionName = ex.getClass().getSimpleName(); log.warn("用户:{} {} from {}", UserContext.getUsername(), message, exceptionName); } } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/web/exceptionhandler/GlobalExceptionHandler.java ================================================ package com.acimage.common.web.exceptionhandler; import com.acimage.common.result.Result; import com.acimage.common.global.exception.BusinessException; import com.acimage.common.utils.ExceptionUtils; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.multipart.MaxUploadSizeExceededException; @Slf4j @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(value={BusinessException.class}) public Result doBusinessException(BusinessException ex){ return Result.fail(ex.getMsg()); } @ExceptionHandler(value={MaxUploadSizeExceededException.class}) public Result doMaxUploadSizeExceededException(MaxUploadSizeExceededException ex){ ExceptionUtils.printIfDev(ex); log.error(ex.getMessage()); return Result.fail("文件大小超出限制"); } @ExceptionHandler(value={RuntimeException.class}) public Result doRuntimeException(RuntimeException ex){ ExceptionUtils.printIfDev(ex); log.error(ex.getMessage()); return Result.fail("未知错误,请刷新重试"); } @ExceptionHandler(value={Exception.class}) public Result doException(Exception ex){ ExceptionUtils.printIfDev(ex); log.error(ex.getMessage()); return Result.fail("未知异常,请刷新重试"); } } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/web/exceptionhandler/JwtExceptionHandler.java ================================================ package com.acimage.common.web.exceptionhandler; import com.acimage.common.utils.SpringContextUtils; import com.acimage.common.utils.common.PageUtils; import com.auth0.jwt.exceptions.JWTVerificationException; import com.acimage.common.result.Code; import com.acimage.common.result.Result; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; @Slf4j @RestControllerAdvice public class JwtExceptionHandler { @ExceptionHandler(value={JWTVerificationException.class}) public Result doTokenException(JWTVerificationException ex){ log.error("{}",ex.getMessage()); return new Result(Code.TOKEN_ERR,null,"登录失效,请重新登录"); } } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/web/interceptor/AccessInterceptor.java ================================================ package com.acimage.common.web.interceptor; import com.acimage.common.global.context.UserContext; import com.acimage.common.utils.IpUtils; import lombok.extern.slf4j.Slf4j; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * 验证token状态和请求所需权限是否匹配 */ @Slf4j public class AccessInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String ip = IpUtils.getIp(request); UserContext.setIp(ip); log.debug("access 用户:{} 访问:{} {} ip:{}", UserContext.getUsername(), request.getRequestURI(), request.getMethod(), ip); return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { HandlerInterceptor.super.postHandle(request, response, handler, modelAndView); } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { //移除用户信息,防止之后用该线程的信息误判(因为线程池) UserContext.remove(); } } ================================================ FILE: acimage_common/src/main/java/com/acimage/common/web/interceptor/JwtInterceptor.java ================================================ package com.acimage.common.web.interceptor; import com.acimage.common.global.consts.HeaderKeyConstants; import com.acimage.common.global.exception.NullTokenException; import com.acimage.common.utils.IpUtils; import com.auth0.jwt.exceptions.JWTVerificationException; import com.acimage.common.global.context.UserContext; import com.acimage.common.service.TokenService; import com.acimage.common.utils.JwtUtils; import com.auth0.jwt.exceptions.TokenExpiredException; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.Date; /** * 获取请求的token状态 */ @Slf4j @Component public class JwtInterceptor implements HandlerInterceptor { @Autowired TokenService tokenService; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String ip = IpUtils.getIp(request); UserContext.setIp(ip); String token = request.getHeader(HeaderKeyConstants.AUTHORIZATION); try { JwtUtils.verifyToken(token); } catch (TokenExpiredException e1) { Date date = JwtUtils.getExpire(token); //过时毫秒数 long expireMillis = System.currentTimeMillis() - date.getTime(); //过时限制不超过10s,可以继续解析token long boundMillis = 10 * 1000; if (expireMillis > boundMillis) { return true; } } catch (JWTVerificationException e2) { if (!(e2 instanceof NullTokenException)) { log.warn("ip:{}非法token", ip); } return true; } if (!tokenService.hasRecorded(token)) { return true; } //设置当前用户信息 UserContext.saveCurrentUserInfo(JwtUtils.getUserId(token), JwtUtils.getUsername(token), JwtUtils.getPhotoUrl(token)); return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { HandlerInterceptor.super.postHandle(request, response, handler, modelAndView); } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { UserContext.remove(); } } ================================================ FILE: acimage_common/src/main/resources/application-common.yml ================================================ spring: mvc: hiddenmethod: filter: enabled: true cloud: sentinel: eager: true transport: dashboard: localhost:7010 #sentinel控制台 #feign支持sentinel feign: sentinel: enabled: true #自定义配置 my-config: enable-mq: true ================================================ FILE: acimage_common/src/main/resources/application-qiniu-template.yml ================================================ #七牛云配置 qiniu: access-key: secret-key: bucket: domain: ================================================ FILE: acimage_common/src/main/resources/lua/getAndCombineAndDelete.lua ================================================ --取出KEYS[1]并加到KEYS[2]的ARGV[1]上(如果KEYS[2]的AVG[1]存在),删除KEYS[1] --返回KEYS[1]对应的值 --KEYS[1] string --KEYS[2] hash --ARGV[1] fieldKey local increment = redis.call('get', KEYS[1]) if not increment then return nil end local base = redis.call('hget',KEYS[2],ARGV[1]) if base then redis.call('hincrby',KEYS[2],ARGV[1],increment) end redis.call('del',KEYS[1]) return increment ================================================ FILE: acimage_common/src/main/resources/lua/incrementIfPresent.lua ================================================ if not redis.call('get', KEYS[1]) then return nil else return redis.call('incrby', KEYS[1], ARGV[1]) end ================================================ FILE: acimage_common/src/main/resources/lua/incrementIfPresentForFieldKey.lua ================================================ --如果KEYS[1]的hash key ARGV[1]存在,则为它增加ARGV[2],并返回增加后的值 --否则返回nil local base = redis.call('hget',KEYS[1],ARGV[1]) if not base then return nil end return redis.call('hincrby',KEYS[1],ARGV[1],ARGV[2]) ================================================ FILE: acimage_common/src/main/resources/lua/incrementIfPresentForZSet.lua ================================================ if not redis.call('zscore', KEYS[1], ARGV[1]) then return nil else -- zincrby key increment member return redis.call('zincrby', KEYS[1], ARGV[2], ARGV[1]) end ================================================ FILE: acimage_common/src/main/resources/lua/requestLimit.lua ================================================ --设n是KEYS长度 --ARGV[1]到ARGV[n]次数限制 --ARGV[n+1]到ARGV[2n]过期时间 --ARGV[2n+1]到ARGV[3n]惩罚过期时间 local result={} local len = #KEYS if (not len) or ( tonumber(len)==0 ) then return result; end for i=1,len do local res=redis.call('incrby',KEYS[i],1) if res==1 then --如果是第一次访问该key则设置过期时间 redis.call('expire',KEYS[i],ARGV[len+i]) end if res==tonumber(ARGV[i])+1 then --超过限制则惩罚过期时间 if tonumber(ARGV[2*len+i])>=0 then redis.call('expire',KEYS[i],ARGV[2*len+i]) end end result[i]=res end return result ================================================ FILE: acimage_common/src/main/resources/lua/setIfPresentForFieldKey.lua ================================================ --如果KEYS[1]的hash key ARGV[1]存在,则将其它设置为ARGV[2],并返回增加后的值 --否则返回nil local base = redis.call('hget',KEYS[1],ARGV[1]) if not base then return nil end if redis.call('hset',KEYS[1],ARGV[1],ARGV[2])==1 then return true else return false end ================================================ FILE: acimage_common/src/main/resources/sensitive_word.txt ================================================ 爱爱 色情 独裁 专政 暴乱 杀人 强奸 嫖娼 吸毒 赌博 做爱 小姐 妓女 包夜 3P 狼友 技师 推油 胸推 毒龙 口爆 楼凤 足交 口暴 口交 SM 桑拿 吞精 咪咪 婊子 乳方 操逼 性伴侣 爱液 按摩棒 爆草 包二奶 暴干 暴奸 暴乳 爆乳 暴淫 被操 被插 逼奸 仓井空 插暴 操逼 操黑 操烂 肏你 肏死 操死 操我 操你 草你妈 厕奴 插比 插b 插逼 插你 插我 插阴 潮吹 潮喷 成人电影 成人论坛 成人色情 成人网站 成人文学 成人小说 艳情小说 成人游戏 吃精 抽插 春药 大波 大力抽送 大乳 荡妇 荡女 盗撮 发浪 放尿 肥逼 粉穴 风月大陆 干死你 干穴 肛交 肛门 龟头 裹本 国产av 好嫩 豪乳 黑逼 后庭 后穴 虎骑 换妻俱乐部 黄片 几吧 鸡吧 鸡巴 鸡奸 奸情 叫床 脚交 精液 巨屌 菊花洞 菊门 巨奶 巨乳 菊穴 开苞 口活 口射 口淫 裤袜 狂操 狂插 浪逼 浪妇 浪叫 浪女 聊性 凌辱 漏乳 露b 乱交 乱伦 轮暴 轮操 轮奸 裸陪 买春 美逼 美少妇 美乳 美穴 美幼 秘唇 迷奸 密穴 蜜穴 蜜液 摸奶 摸胸 母奸 奈美 奶子 男奴 内射 嫩逼 嫩女 嫩穴 捏弄 女优 喷精 屁眼 强jian 强暴 强奸处女 情趣用品 情色 拳交 群交 人妻 人兽 日逼 日烂 肉棒 肉逼 肉唇 肉洞 肉缝 肉棍 肉茎 肉具 揉乳 肉穴 肉欲 乳爆 乳房 乳沟 乳交 乳头 骚逼 骚比 骚女 骚水 骚穴 色逼 色界 色猫 色盟 色情网站 色区 色诱 色欲 色b 少年阿宾 射爽 射颜 食精 释欲 兽奸 兽交 手淫 傻逼 傻b 傻吊 傻叉 兽欲 熟妇 熟母 熟女 爽片 双臀 死逼 丝袜 丝诱 松岛枫 酥痒 汤加丽 套弄 体奸 体位 舔脚 舔阴 偷欢 脱内裤 文做 舞女 吸精 夏川纯 相奸 小逼 校鸡 小穴 小xue 性感妖娆 性感诱惑 性虎 性饥渴 性技巧 性交 性奴 性虐 性息 性欲 胸推 穴口 穴图 亚情 颜射 阳具 杨思敏 要射了 夜勤病栋 一本道 一夜欢 一夜情 一ye情 阴部 淫虫 阴唇 淫荡 阴道 淫电影 阴阜 淫妇 淫河 阴核 阴户 淫贱 淫叫 淫教师 阴茎 阴精 淫浪 淫媚 淫糜 淫魔 淫母 淫女 淫虐 淫妻 淫情 淫色 淫声浪语 淫兽学园 淫书 淫术炼金士 淫水 淫娃 淫威 淫亵 淫样 淫液 淫照 阴b 应召 幼交 欲火 欲女 玉乳 玉穴 援交 原味内衣 援助交际 招鸡 招妓 抓胸 自慰 作爱 a片 bitch fuck gay片 g点 h动画 h动漫 失身粉 淫荡自慰器 习近平 平近习 xjp 习太子 习明泽 老习 温家宝 温加宝 温x 温jia宝 温宝宝 温加饱 温加保 张培莉 温云松 温如春 温jb 胡温 胡x 胡jt 胡boss 胡总 胡王八 hujintao 胡jintao 胡j涛 胡惊涛 胡景涛 胡紧掏 湖紧掏 胡紧套 锦涛 hjt 胡派 胡主席 刘永清 胡海峰 胡海清 江泽民 民泽江 江胡 江哥 江主席 江书记 江浙闽 江沢民 江浙民 择民 则民 茳泽民 zemin ze民 老江 老j 江core 江x 江派 江zm jzm 江戏子 江蛤蟆 江某某 江贼 江猪 江氏集团 江绵恒 江绵康 王冶坪 江泽慧 邓小平 平小邓 xiao平 邓xp 邓晓平 邓朴方 邓榕 邓质方 毛泽东 猫泽东 猫则东 猫贼洞 毛zd 毛zx z东 ze东 泽d zedong 毛太祖 毛相 主席画像 改革历程 朱镕基 朱容基 朱镕鸡 朱容鸡 朱云来 李鹏 李peng 里鹏 李月月鸟 李小鹏 李小琳 华主席 华国 国锋 国峰 锋同志 白春礼 薄熙来 薄一波 蔡赴朝 蔡武 曹刚川 常万全 陈炳德 陈德铭 陈建国 陈良宇 陈绍基 陈同海 陈至立 戴秉国 丁一平 董建华 杜德印 杜世成 傅锐 郭伯雄 郭金龙 贺国强 胡春华 耀邦 华建敏 黄华华 黄丽满 黄兴国 回良玉 贾庆林 贾廷安 靖志远 李长春 李春城 李建国 李克强 李岚清 李沛瑶 李荣融 李瑞环 李铁映 李先念 李学举 李源潮 栗智 梁光烈 廖锡龙 林树森 林炎志 林左鸣 令计划 柳斌杰 刘奇葆 刘少奇 刘延东 刘云山 刘志军 龙新民 路甬祥 罗箭 吕祖善 马飚 马恺 孟建柱 欧广源 强卫 沈跃跃 宋平顺 粟戎生 苏树林 孙家正 铁凝 屠光绍 王东明 汪东兴 王鸿举 王沪宁 王乐泉 王洛林 王岐山 王胜俊 王太华 王学军 王兆国 王振华 吴邦国 吴定富 吴官正 无官正 吴胜利 吴仪 奚国华 习仲勋 徐才厚 许其亮 徐绍史 杨洁篪 叶剑英 由喜贵 于幼军 俞正声 袁纯清 曾培炎 曾庆红 曾宪梓 曾荫权 张德江 张定发 张高丽 张立昌 张荣坤 张志国 赵洪祝 紫阳 周生贤 周永康 朱海仑 中南海 大陆当局 中国当局 北京当局 共产党 党产共 共贪党 阿共 产党共 公产党 工产党 共c党 共x党 共铲 供产 共惨 供铲党 供铲谠 供铲裆 共残党 共残主义 共产主义的幽灵 拱铲 老共 中共 中珙 中gong gc党 贡挡 gong党 g产 狗产蛋 共残裆 恶党 邪党 共产专制 共产王朝 裆中央 土共 土g 共狗 g匪 共匪 仇共 政府 症腐 政腐 政付 正府 政俯 政f zhengfu 政zhi 挡中央 档中央 中央领导 中国zf 中央zf 国wu院 中华帝国 gong和 大陆官方 北京政权 胡锦涛 习仲勋 贺国强 贺子珍 周永康 李长春 李德生 王岐山 姚依林 回良玉 李源潮 李干成 戴秉国 黄镇 刘延东 刘瑞龙 俞正声 黄敬 薄熙 薄一波 周小川 周建南 温云松 徐明 江泽慧 江绵恒 江绵康 李小鹏 李鹏 李小琳 朱云来 朱容基 李洪志 新疆骚乱 腐败中国 三个呆婊 你办事我放心 社会主义灭亡 打倒中国 打倒共产党 打倒共产主义 打倒胡锦涛 打倒江泽民 打倒江主席 打倒李鹏 打倒罗干 打倒温家宝 打倒中共 打倒朱镕 抵制共产党 抵制共产主义 抵制胡锦涛 抵制江泽民 抵制江主席 抵制李鹏 抵制罗干 抵制温家宝 抵制中共 抵制朱镕基 灭亡中国 亡党亡国 粉碎四人帮 激流中国 特供 特贡 特共 zf大楼 殃视 贪污腐败 强制拆除 形式主义 政治风波 太子党 上海帮 北京帮 清华帮 红色贵族 权贵集团 河蟹社会 喝血社会 九风 9风 十七大 十7大 17da 九学 9学 四风 4风 双规 南街村 最淫官员 警匪 官匪 独夫民贼 官商勾结 城管暴力执法 强制捐款 毒豺 一党执政 一党专制 一党专政 专制政权 宪法 法院 胡平 苏晓康 贺卫方 谭作人 焦国标 万润南 张志新 辛灝年 高勤荣 王炳章 高智晟 司马璐 刘晓竹 刘宾雁 魏京生 寻找林昭的灵魂 别梦成灰 谁是新中国 讨伐中宣部 异议人士 民运人士 启蒙派 选国家主席 民一主 min主 民竹 民珠 民猪 chinesedemocracy 大赦国际 国际特赦 da选 投公 公头 宪政 平反 党章 维权 昝爱宗 宪章 08宪 08xz 抿主 敏主 人拳 人木又 人quan renquan 中国人权 中国新民党 群体事件 群体性事件 上中央 去中央 讨说法 请愿 请命 公开信 联名上书 万人大签名 万人骚动 截访 上访 shangfang 信访 访民 集合 集会 组织集体 静坐 静zuo jing坐 示威 示wei 游行 you行 油行 游xing youxing 官逼民反 反party 反共 抗议 亢议 抵制 低制 底制 di制 抵zhi dizhi boycott 血书 焚烧中国国旗 baoluan 流血冲突 出现暴动 发生暴动 引起暴动 baodong 灭共 杀毙 罢工 霸工 罢考 罢餐 霸餐 罢参 罢饭 罢吃 罢食 罢课 罢ke 霸课 ba课 罢教 罢学 罢运 网特 网评员 网络评论员 五毛党 五毛们 5毛党 戒严 jieyan jie严 戒yan 8的平方事件 知道64 八九年 贰拾年 2o年 20和谐年 贰拾周年 六四 六河蟹四 六百度四 六和谐四 陆四 陆肆 198964 5月35 89年春夏之交 64惨案 64时期 64运动 4事件 四事件 北京风波 学潮 学chao xuechao 学百度潮 门安天 天按门 坦克压大学生 民主女神 历史的伤口 高自联 北高联 血洗京城 四二六社论 王丹 柴玲 沈彤 封从德 王超华 王维林 吾尔开希 吾尔开西 侯德健 阎明复 方励之 蒋捷连 丁子霖 辛灏年 蒋彦永 严家其 陈一咨 中华局域网 党的喉舌 互联网审查 当局严密封锁 新闻封锁 封锁消息 爱国者同盟 关闭所有论坛 网络封锁 金盾工程 gfw 无界浏览 无界网络 自由门 何清涟 中国的陷阱 汪兆钧 记者无疆界 境外媒体 维基百科 纽约时报 bbc中文网 华盛顿邮报 世界日报 东森新闻网 东森电视 星岛日报 wikipedia youtube googleblogger 美国广播公司 英国金融时报 自由亚洲 自由时报 中国时报 反分裂 威胁论 左翼联盟 钓鱼岛 保钓组织 主权 弓单 火乍 木仓 石肖 核蛋 步qiang bao炸 爆zha baozha zha药 zha弹 炸dan 炸yao zhadan zhayao hmtd 三硝基甲苯 六氟化铀 炸药配方 弹药配方 炸弹配方 皮箱炸弹 火药配方 人体炸弹 人肉炸弹 解放军 兵力部署 军转 军事社 8341部队 第21集团军 七大军区 7大军区 北京军区 沈阳军区 济南军区 成都军区 广州军区 南京军区 兰州军区 颜色革命 规模冲突 塔利班 基地组织 恐怖分子 恐怖份子 三股势力 印尼屠华 印尼事件 蒋公纪念歌 马英九 mayingjiu 李天羽 苏贞昌 林文漪 陈水扁 陈s扁 陈随便 阿扁 a扁 告全国同胞书 台百度湾 台完 台wan taiwan 台弯 湾台 台湾国 台湾共和国 台军 台独 台毒 台du taidu twdl 一中一台 打台湾 两岸战争 攻占台湾 支持台湾 进攻台湾 占领台湾 统一台湾 收复台湾 登陆台湾 解放台湾 解放tw 解决台湾 光复民国 台湾独立 台湾问题 台海问题 台海危机 台海统一 台海大战 台海战争 台海局势 入联 入耳关 中华联邦 国民党 x民党 民进党 青天白日 闹独立 duli fenlie 日本万岁 小泽一郎 劣等民族 汉人 汉维 维汉 维吾 吾尔 热比娅 伊力哈木 疆独 东突厥斯坦解放组织 东突解放组织 蒙古分裂分子 列确 阿旺晋美 藏人 臧人 zang人 藏民 藏m 达赖 赖达 dalai 哒赖 dl喇嘛 丹增嘉措 打砸抢 西独 藏独 葬独 臧独 藏毒 藏du zangdu 支持zd 藏暴乱 藏青会 雪山狮子旗 拉萨 啦萨 啦沙 啦撒 拉sa lasa la萨 西藏 藏西 藏春阁 藏獨 藏独 藏独立 藏妇会 藏青会 藏字石 xizang xi藏 x藏 西z tibet 希葬 希藏 硒藏 稀藏 西脏 西奘 西葬 西臧 援藏 王千源 安拉 回教 回族 回回 回民 穆斯林 穆罕穆德 穆罕默德 默罕默德 伊斯兰 圣战组织 清真 真主 阿拉伯 高丽棒子 韩国狗 满洲第三帝国 满狗 鞑子 江丑闻 江嫡系 江毒 江独裁 江蛤蟆 江核心 江黑心 江胡内斗 江祸心 江家帮 江绵恒 江派和胡派 江派人马 江泉集团 江人马 江三条腿 江氏家族 江氏政治局 江氏政治委员 江梳头 江太上 江戏子 江系人 江系人马 江宰民 江贼 江贼民 江主席 麻果丸 麻将透 麻醉弹 麻醉狗 麻醉枪 麻醉槍 麻醉药 麻醉藥 台湾版假币 台湾独立 台湾应该独立 台湾有权独立 天灭中共 中共帮凶 中共保命 中共裁 中共党文化 中共腐败 中共的血旗 中共的罪恶 中共帝国 中共独裁 中共封锁 中共封网 中共黑 中共黑帮 中共解体 中共近期权力斗争 中共恐惧 中共权力斗争 中共任用 中共退党 中共洗脑 中共邪教 中共邪毒素 中共政治游戏 ================================================ FILE: acimage_community/pom.xml ================================================ acimage com.acimage 0.0.1-SNAPSHOT 4.0.0 acimage_community acimage_community http://www.example.com com.acimage acimage_common 0.0.1-SNAPSHOT com.acimage acimage_feign 0.0.1-SNAPSHOT org.springframework.boot spring-boot-starter-aop com.alibaba druid mysql mysql-connector-java runtime org.springframework.boot spring-boot-starter-amqp org.springframework.boot spring-boot-starter-quartz io.minio minio io.github.toolgood toolgood-words org.springframework.data spring-data-elasticsearch com.github.ben-manes.caffeine caffeine com.github.nintha webp-imageio-core 0.1.0 system ${pom.basedir}/lib/webp-imageio-core-0.1.0.jar org.springframework.boot spring-boot-maven-plugin true org.apache.maven.plugins maven-surefire-plugin true ================================================ FILE: acimage_community/src/main/java/com/acimage/community/CommunityApplication.java ================================================ package com.acimage.community; import lombok.extern.slf4j.Slf4j; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.openfeign.EnableFeignClients; import org.springframework.context.annotation.ComponentScan; import org.springframework.scheduling.annotation.EnableScheduling; @Slf4j @SpringBootApplication @EnableDiscoveryClient @EnableScheduling @EnableFeignClients(basePackages="com.acimage.feign") @MapperScan("com.acimage.community.dao") @ComponentScan(value={"com.acimage"}) public class CommunityApplication { public static void main(String[] args) { SpringApplication.run(CommunityApplication.class, args); log.info("------------->>>Community启动<<<-------------"); } } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/dao/CategoryDao.java ================================================ package com.acimage.community.dao; import com.acimage.common.model.domain.community.Category; import com.baomidou.mybatisplus.core.mapper.BaseMapper; public interface CategoryDao extends BaseMapper { } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/dao/CmtyUserDao.java ================================================ package com.acimage.community.dao; import cn.hutool.core.lang.Pair; import com.acimage.common.model.domain.community.CmtyUser; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; import org.apache.ibatis.annotations.Update; import java.util.List; public interface CmtyUserDao extends BaseMapper { // @Select("select * from tb_user_basic where id=#{id}") // CmtyUser selectCmtyUserById(@Param("id") long id); Integer batchUpdateStarCount(@Param("userIdAndIncrements") List> userIdAndIncrements); Integer batchUpdateTopicCount(@Param("userIdAndIncrements") List> userIdAndIncrements); @Update("update tb_cmty_user set topic_count=topic_count+#{increment} where id=#{userId}") Integer updateTopicCountByIncrement(@Param("userId") long userId, @Param("increment") int increment); @Update("update tb_cmty_user set star_count=star_count+#{increment} where id=#{userId}") Integer updateStarCountByIncrement(@Param("userId") long userId, @Param("increment") int increment); @Select("select * from tb_cmty_user order by ${column} desc limit #{startIndex},#{recordNum}") List selectListOrderByColumn(@Param("column") String underlineColumnName, @Param("startIndex") int startIndex, @Param("recordNum") Integer recordNum); } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/dao/CommentDao.java ================================================ package com.acimage.community.dao; import com.acimage.common.model.domain.community.Comment; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Delete; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; import org.apache.ibatis.annotations.Update; import java.util.List; public interface CommentDao extends BaseMapper { @Delete("delete from tb_comment where topic_id=#{topicId}") Integer deleteByTopicId(@Param("topicId") long topicId); @Update("update tb_comment set content=#{content},update_time=now() where id=#{id}") Integer updateContentById(@Param("id") long id, @Param("content") String content); List selectCommentsWithUserByTopicId(@Param("topicId") long topicId); @Select("select count(*) as comment_count from tb_comment where topic_id=#{topicId}") Integer countCommentsByTopicId(@Param("topicId") long topicId); @Select("select count(*) as count from tb_comment where user_id=#{userId}") Integer countCommentsByUserId(@Param("userId") long userId); List selectCommentsWithUser(@Param("topicId") long topicId,@Param("startIndex") int startIndex,@Param("recordNumber") int recordNumber); List selectCommentsWithTopicOrderByCreateTime(@Param("userId") long userId, @Param("startIndex") int startIndex, @Param("recordNumber") int recordNumber); } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/dao/HomeCarrouselDao.java ================================================ package com.acimage.community.dao; import com.acimage.common.model.domain.community.HomeCarousel; import com.baomidou.mybatisplus.core.mapper.BaseMapper; public interface HomeCarrouselDao extends BaseMapper { } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/dao/ImageDao.java ================================================ package com.acimage.community.dao; import cn.hutool.core.lang.Pair; import com.acimage.common.model.domain.image.Image; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; import java.util.List; public interface ImageDao extends BaseMapper { Integer insertList(List images); Integer updateDescription(List> idAndDescriptions); @Select("select * from tb_image where topic_id=#{topicId} order by id") List selectListOrderById(@Param("topicId") long topicId); List selectImagesWithTopic(List imageIds); } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/dao/StarDao.java ================================================ package com.acimage.community.dao; import com.acimage.common.model.domain.community.Star; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Delete; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; import java.util.List; public interface StarDao extends BaseMapper { @Delete("delete from tb_star where user_id=#{userId} and topic_id=#{topicId}") Integer deleteByUserIdAndTopicId(@Param("userId") long userId, @Param("topicId") long topicId); @Delete("delete from tb_star where topic_id=#{topicId}") Integer deleteByTopicId(@Param("topicId") long topicId); // @Select("select * from tb_star where user_id=#{userId} and topic_id=#{topicId}") // Star selectOne(@Param("userId") long userId,@Param("topicId") long topicId); List selectStarsWithTopicOrderByCreateTime(@Param("userId") long userId, @Param("startIndex") int startIndex, @Param("recordNumber") int recordNumber); Integer countStarsOwnedBy(@Param("userId") long userId); @Select("select count(*) as star_count from tb_star where topic_id=#{topicId}") Integer countStarsByTopicId(@Param("topicId") long topicId); @Select("select count(*) as star_count from tb_star where user_id=#{userId}") Integer countStarsByUserId(@Param("userId") long userId); } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/dao/TagDao.java ================================================ package com.acimage.community.dao; import com.acimage.common.model.domain.community.Tag; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Select; import java.util.List; public interface TagDao extends BaseMapper { } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/dao/TagTopicDao.java ================================================ package com.acimage.community.dao; import com.acimage.common.model.domain.community.TagTopic; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; import java.util.List; public interface TagTopicDao extends BaseMapper { void insertBatch(List tagTopicList); @Select("select tag_id from tb_tag_topic where topic_id=#{topicId} and deleted=0") List selectTagIds(@Param("topicId") long topicId); } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/dao/TopicDao.java ================================================ package com.acimage.community.dao; import cn.hutool.core.lang.Pair; import com.acimage.common.model.domain.community.Topic; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; import org.apache.ibatis.annotations.Update; import reactor.util.annotation.Nullable; import java.util.Date; import java.util.List; public interface TopicDao extends BaseMapper { @Update("update tb_topic set title=#{title},content=#{content},update_time=now() where id=#{id} and deleted=0") Integer updateTopic(@Param("id") long id, @Param("title") String title, @Param("content") String content); Integer updatePvByIncrement(@Param("idAndIncrements") List> idAndIncrements); Integer batchUpdateColumnByIncrement(@Param("column") String underlineColumnName, @Param("idAndIncrements") List> idAndIncrements); @Update("update tb_topic set ${column}=${column}+#{increment} where id=#{id} and deleted=0") Integer updateColumnByIncrement(@Param("column") String column, @Param("id") long id, @Param("increment") int increment); @Update("update tb_topic set activity_time=#{activityTime} where id=#{id} and deleted=0") Integer updateActivityTime(@Param("id") long id, @Param("activityTime") Date activityTime); Integer batchUpdateActivityTime(@Param("idAndActivityTimes") List> idAndActivityTimes); List selectTopicsWithUserOrderByPageView(@Param("startTime") String startTime, @Nullable @Param("limit") Integer limit); List selectTopicsWithUserOrderBy(@Param("column") String columnForOrder, @Param("limit") int limit); Topic selectTopicWithUser(@Param("id") long id); List selectTopicsWithUserOrderByCreateTime(@Param("userId") long userId, @Param("startIndex") int startIndex, @Param("recordNumber") int recordNumber); List selectTopicsWithUserByIds(@Param("ids") List ids); @Select("select count(*) from tb_topic where user_id=#{userId} and deleted=0") Integer countTopics(@Param("userId") long userId); } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/dao/TopicHtmlDao.java ================================================ package com.acimage.community.dao; import com.acimage.common.model.domain.community.TopicHtml; import com.baomidou.mybatisplus.core.mapper.BaseMapper; public interface TopicHtmlDao extends BaseMapper { } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/depreted/CmtyUserDaoBak.java ================================================ package com.acimage.community.depreted; import cn.hutool.core.lang.Pair; import com.acimage.common.deprecated.UserCommunityStatistic; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Update; import java.util.List; public interface CmtyUserDaoBak extends BaseMapper { Integer batchUpdateStarCount(@Param("userIdAndIncrements") List> userIdAndIncrements); Integer batchUpdateTopicCount(@Param("userIdAndIncrements") List> userIdAndIncrements); @Update("update tb_cmty_user set topic_count=topic_count+#{increment} where user_id=#{userId}") Integer updateTopicCountByIncrement(@Param("userId") long userId, @Param("increment") int increment); @Update("update tb_cmty_user set star_count=star_count+#{increment} where user_id=#{userId}") Integer updateStarCountByIncrement(@Param("userId") long userId, @Param("increment") int increment); List selectListOrderByColumn(@Param("column") String underlineColumnName, @Param("startIndex") int startIndex, @Param("recordNum") Integer recordNum); } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/depreted/RabbitmqConvertConfig.java ================================================ package com.acimage.community.depreted; import org.springframework.amqp.core.AcknowledgeMode; import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory; import org.springframework.amqp.rabbit.connection.ConnectionFactory; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.amqp.rabbit.listener.RabbitListenerContainerFactory; import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; import org.springframework.context.annotation.Bean; @Deprecated public class RabbitmqConvertConfig { //发送方序列化 @Bean public RabbitTemplate jacksonRabbitTemplate(ConnectionFactory connectionFactory) { RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory); rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter()); return rabbitTemplate; } //消费者序列化 @Bean public RabbitListenerContainerFactory rabbitListenerContainerFactory(ConnectionFactory connectionFactory){ SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); factory.setConnectionFactory(connectionFactory); factory.setMessageConverter(new Jackson2JsonMessageConverter()); factory.setAcknowledgeMode(AcknowledgeMode.MANUAL); return factory; } } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/depreted/UserMixWriteService.java ================================================ package com.acimage.community.depreted; import com.acimage.common.model.domain.community.CmtyUser; public interface UserMixWriteService { void addUserBasicAndUserCommunityStatistic(CmtyUser cmtyUser); } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/depreted/UserMixWriteServiceImpl.java ================================================ package com.acimage.community.depreted; import com.acimage.common.model.domain.community.CmtyUser; import com.acimage.community.service.cmtyuser.CmtyUserWriteService; import com.acimage.community.depreted.UserMixWriteService; import com.acimage.community.depreted.userstatistic.UserCsWriteService; import org.springframework.beans.factory.annotation.Autowired; public class UserMixWriteServiceImpl implements UserMixWriteService { @Autowired CmtyUserWriteService cmtyUserWriteService; @Autowired UserCsWriteService userCsWriteService; @Override public void addUserBasicAndUserCommunityStatistic(CmtyUser cmtyUser){ cmtyUserWriteService.save(cmtyUser); userCsWriteService.save(cmtyUser.getId()); } } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/depreted/userstatistic/UserCsQueryService.java ================================================ package com.acimage.community.depreted.userstatistic; import com.acimage.common.deprecated.UserCommunityStatistic; import com.acimage.common.redis.annotation.QueryRedis; import com.acimage.community.depreted.userstatistic.consts.KeyConstants; public interface UserCsQueryService { @QueryRedis(keyPrefix = KeyConstants.STRINGKP_CMTY_USER,expire = 10L) UserCommunityStatistic getUserCommunityStatistic(long userId); } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/depreted/userstatistic/UserCsRankService.java ================================================ package com.acimage.community.depreted.userstatistic; import com.acimage.common.model.domain.user.User; import com.acimage.common.redis.annotation.QueryRedis; import java.util.List; public interface UserCsRankService { @QueryRedis(keyPrefix = "acimage:community:users:rank:topicCount",expire = 5L) List pageUserRankByTopicCount(int pageNo, int pageSize); @QueryRedis(keyPrefix = "acimage:community:users:rank:starCount",expire = 5L) List pageUserRankByStarCount(int pageNo, int pageSize); } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/depreted/userstatistic/UserCsWriteService.java ================================================ package com.acimage.community.depreted.userstatistic; import org.apache.ibatis.annotations.Param; import java.util.List; public interface UserCsWriteService { Integer updateStarCountByIncrements(List userIds, List starCounts); Integer updateTopicCountByIncrements(List userIds, List starCounts); Integer updateTopicCountByIncrement(long userId, int increment); Integer updateStarCountByIncrement(long userId, int increment); void save(long userId); } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/depreted/userstatistic/consts/KeyConstants.java ================================================ package com.acimage.community.depreted.userstatistic.consts; public class KeyConstants { public static final String STRINGKP_CMTY_USER ="acimage:community:cmtyUser:userId:"; } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/depreted/userstatistic/impl/UserCsQueryServiceImpl.java ================================================ package com.acimage.community.depreted.userstatistic.impl; import com.acimage.common.deprecated.UserCommunityStatistic; import com.acimage.common.redis.annotation.QueryRedis; import com.acimage.common.redis.enums.DataType; import com.acimage.community.depreted.CmtyUserDaoBak; import com.acimage.community.depreted.userstatistic.consts.KeyConstants; import com.acimage.community.depreted.userstatistic.UserCsQueryService; import org.springframework.beans.factory.annotation.Autowired; public class UserCsQueryServiceImpl implements UserCsQueryService { @Autowired CmtyUserDaoBak userCsDao; @Override @QueryRedis(keyPrefix = KeyConstants.STRINGKP_CMTY_USER, expire = 10L, dataType = DataType.HASH) public UserCommunityStatistic getUserCommunityStatistic(long userId) { return userCsDao.selectById(userId); } } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/depreted/userstatistic/impl/UserCsRankServiceImpl.java ================================================ package com.acimage.community.depreted.userstatistic.impl; import com.acimage.common.model.domain.user.User; import com.acimage.common.deprecated.UserCommunityStatistic; import com.acimage.common.redis.annotation.QueryRedis; import com.acimage.common.utils.LambdaUtils; import com.acimage.common.utils.common.PageUtils; import com.acimage.community.depreted.CmtyUserDaoBak; import com.acimage.community.depreted.userstatistic.UserCsRankService; import com.baomidou.mybatisplus.core.toolkit.support.SFunction; import org.springframework.beans.factory.annotation.Autowired; import java.util.ArrayList; import java.util.List; public class UserCsRankServiceImpl implements UserCsRankService { @Autowired CmtyUserDaoBak userCsDao; private List pageUserRankBy(SFunction attr, int pageNo, int pageSize){ int start= PageUtils.startIndexOf(pageNo,pageSize); String column= LambdaUtils.underlineColumnNameOf(attr); List userCsList= userCsDao.selectListOrderByColumn(column,start,pageSize); List users =new ArrayList<>(); for(UserCommunityStatistic userCs:userCsList){ User user =new User(); user.setTopicCount(userCs.getTopicCount()); user.setStarCount(userCs.getStarCount()); user.setId(userCs.getUserId()); user.setUsername(userCs.getCmtyUser().getUsername()); user.setPhotoUrl(userCs.getCmtyUser().getPhotoUrl()); users.add(user); } return users; } @Override @QueryRedis(keyPrefix = "acimage:community:users:rank:topicCount:",expire = 5L) public List pageUserRankByTopicCount(int pageNo, int pageSize){ return pageUserRankBy(UserCommunityStatistic::getTopicCount,pageNo,pageSize); } @Override @QueryRedis(keyPrefix = "acimage:community:users:rank:starCount:",expire = 5L) public List pageUserRankByStarCount(int pageNo, int pageSize){ return pageUserRankBy(UserCommunityStatistic::getStarCount,pageNo,pageSize); } } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/depreted/userstatistic/impl/UserCsWriteServiceImpl.java ================================================ package com.acimage.community.depreted.userstatistic.impl; import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.lang.Pair; import com.acimage.common.deprecated.UserCommunityStatistic; import com.acimage.common.utils.redis.RedisUtils; import com.acimage.common.utils.common.PairUtils; import com.acimage.community.depreted.CmtyUserDaoBak; import com.acimage.community.depreted.userstatistic.consts.KeyConstants; import com.acimage.community.depreted.userstatistic.UserCsWriteService; import org.springframework.beans.factory.annotation.Autowired; import java.util.List; public class UserCsWriteServiceImpl implements UserCsWriteService { @Autowired CmtyUserDaoBak userCsDao; @Autowired RedisUtils redisUtils; @Override public Integer updateStarCountByIncrements(List userIds, List starCounts){ List> userIdAndStarCounts= PairUtils.combine(userIds,starCounts); if(CollectionUtil.isEmpty(userIds)){ return 0; } return userCsDao.batchUpdateStarCount(userIdAndStarCounts); } @Override public Integer updateTopicCountByIncrements(List userIds, List starCounts){ List> userIdAndStarCounts= PairUtils.combine(userIds,starCounts); if(CollectionUtil.isEmpty(userIds)){ return 0; } return userCsDao.batchUpdateTopicCount(userIdAndStarCounts); } @Override public Integer updateTopicCountByIncrement( long userId, int increment){ redisUtils.delete(KeyConstants.STRINGKP_CMTY_USER +userId); return userCsDao.updateTopicCountByIncrement(userId,increment); } @Override public Integer updateStarCountByIncrement(long userId, int increment) { redisUtils.delete(KeyConstants.STRINGKP_CMTY_USER +userId); return userCsDao.updateStarCountByIncrement(userId,increment); } @Override public void save(long userId){ UserCommunityStatistic userCs=new UserCommunityStatistic(); userCs.setUserId(userId); userCsDao.insert(userCs); } } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/esdao/UserEsDao.java ================================================ package com.acimage.community.esdao; import com.acimage.common.model.Index.TopicIndex; import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; import org.springframework.stereotype.Repository; @Repository public interface UserEsDao extends ElasticsearchRepository { } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/global/annotation/RecordPageView.java ================================================ package com.acimage.community.global.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 带有该注解的接口,被调用时会记录到对应话题的浏览量中 */ @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface RecordPageView { } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/global/annotation/TopicId.java ================================================ package com.acimage.community.global.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 注解在参数或javabean对象的成员变量上 */ @Target({ElementType.FIELD,ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) public @interface TopicId { } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/global/aop/RecordPageViewAdvice.java ================================================ package com.acimage.community.global.aop; import com.acimage.common.global.context.UserContext; import com.acimage.common.utils.common.AopUtils; import com.acimage.common.utils.redis.RedisUtils; import com.acimage.community.global.annotation.TopicId; import com.acimage.community.service.topic.TopicSpAttrQueryService; import com.acimage.community.global.consts.TopicKeyConstants; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.AfterReturning; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; @Aspect @Component @Slf4j @Order(10) public class RecordPageViewAdvice { private static final String POINT_CUT= "@annotation(com.acimage.community.global.annotation.RecordPageView)"; @Autowired RedisUtils redisUtils; @Autowired TopicSpAttrQueryService topicSpAttrQueryService; @Pointcut(POINT_CUT) public void recordPageViewPointCut() { } @AfterReturning(pointcut = "recordPageViewPointCut()", returning = "result") public Object around(JoinPoint joinPoint, Object result) throws Throwable { Long topicId=AopUtils.annotatedArgOrArgFieldOf(joinPoint, TopicId.class, Long.class); if (topicId==null) { return result; } String ipAddress = UserContext.getIp(); // 获取存入的key String topicPvLogKey = TopicKeyConstants.LOGKP_TOPIC_PV + topicId; //记录哪些ip访问过这个话题,以下这两行代码顺序不可交换! Long count = redisUtils.addForHyperLogLog(topicPvLogKey, ipAddress); //记录当前哪些话题有在统计浏览量 redisUtils.addForSet(TopicKeyConstants.SETK_RECORDING_PV_INCREMENT, topicId.toString()); if (count == 0) { log.debug("ip:{} 已访问过 话题{}", ipAddress, topicId); } else { log.debug("ip:{} 访问话题{} 累计访问量 {}", ipAddress, topicId, redisUtils.sizeForHyperLogLog(topicPvLogKey)); //更新浏览量排行榜 redisUtils.addForZSet(TopicKeyConstants.ZSETK_TOPIC_PV, topicId.toString(), topicSpAttrQueryService.getPageView(topicId)); } return result; } } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/global/config/JobFactory.java ================================================ package com.acimage.community.global.config; import org.quartz.spi.TriggerFiredBundle; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.AutowireCapableBeanFactory; import org.springframework.scheduling.quartz.AdaptableJobFactory; public class JobFactory extends AdaptableJobFactory { @Autowired private AutowireCapableBeanFactory capableBeanFactory; @Override protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception { // 调用父类的方法 Object jobInstance = super.createJobInstance(bundle); // 进行注入 capableBeanFactory.autowireBean(jobInstance); return jobInstance; } } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/global/config/WebMvcConfig.java ================================================ package com.acimage.community.global.config; import com.acimage.common.web.interceptor.JwtInterceptor; import com.acimage.common.web.interceptor.AccessInterceptor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Slf4j @Configuration public class WebMvcConfig implements WebMvcConfigurer { @Autowired JwtInterceptor jwtInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(jwtInterceptor).addPathPatterns("/**").order(20); registry.addInterceptor(new AccessInterceptor()).addPathPatterns("/**").order(30); } } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/global/consts/CommentKeyConstants.java ================================================ package com.acimage.community.global.consts; public class CommentKeyConstants { public static final String STRINGKP_COMMENT_COUNT = "acimage:community:comments:commentCount:topicId:"; public static final String STRINGKP_TOPIC_COMMENTS = "acimage:community:comments:topicId:pageNo:"; public static final String STRINGKP_USER_COMMENTS = "acimage:community:comments:userId:pageNo:"; public static final String STRINGKP_PUBLISHED_COMMENTS = "acimage:community:comments:published:userId:"; public static String keyOfTopicComments(long topicId, int pageNo) { return String.format("%s%s:%s", STRINGKP_TOPIC_COMMENTS, topicId, pageNo); } } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/global/consts/CoverImageConstants.java ================================================ package com.acimage.community.global.consts; public class CoverImageConstants { public static final int WIDTH=405; public static final int HEIGHT=390; //压缩后大小不能超过40kb public static final int LIMIT_COMPRESS_SIZE =40*1000; } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/global/consts/PageSizeConstants.java ================================================ package com.acimage.community.global.consts; public class PageSizeConstants { public static final int TOPIC_COMMENTS =10; public static final int ACTIVITY_TOPICS =5; public static final int ACTIVITY_COMMENTS =5; public static final int ACTIVITY_STARS =5; public static final int FORUM_TOPICS =12; } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/global/consts/StarKeyConstants.java ================================================ package com.acimage.community.global.consts; public class StarKeyConstants { public static final String STRINGKP_TOPIC_STAR_COUNT = "acimage:community:stars:starCount:topicId:"; public static final String STRINGKP_USER_STAR_COUNT = "acimage:community:stars:starCount:userId:"; public static final String STRINGKP_STAR_USER_TOPIC = "acimage:community:stars:isStar:userId:topicId:"; public static String keyOfIsStar(long userId, long topicId) { return STRINGKP_STAR_USER_TOPIC + userId + ":" + topicId; } } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/global/consts/TopicKeyConstants.java ================================================ package com.acimage.community.global.consts; public class TopicKeyConstants { public static final String HASHKP_TOPIC = "acimage:community:topics:id:"; public static final String HASHKP_TOPIC_HTML="acimage:community:topicHtml:topicId:"; public static final String STRINGKP_PUBLISHED_TOPIC_TITLE="acimage:community:topics:published:userId:"; public static final String STRINGKP_TOPIC_STAR_COUNT_INCREMENT = "acimage:community:topics:starCountIncrement:id:"; public static final String STRINGKP_TOPIC_COMMENT_COUNT_INCREMENT = "acimage:community:topics:commentCountIncrement:id:"; public static final String STRINGKP_TOPIC_ACTIVITY_TIME = "acimage:community:topics:activityTime:id:"; /** * key前缀,统计浏览了对应话题ip数,redis类型:HyperLogLog */ public static final String LOGKP_TOPIC_PV ="acimage:community:topics:pageViewLog:id:"; /** * key,记录所有被记录浏览量增量的话题id, redis类型:set */ public static final String SETK_RECORDING_PV_INCREMENT ="acimage:community:topics:recording:pageViewLog"; /** * key,记录所有被记录收藏量增量的话题id, redis类型:set */ public static final String SETK_RECORDING_STAR_COUNT_INCREMENT ="acimage:community:topics:recording:starCountIncrement"; /** * key,记录所有被记录评论数增量的话题id, redis类型:set */ public static final String SETK_RECORDING_COMMENT_COUNT_INCREMENT ="acimage:community:topics:recording:commentCountIncrement"; public static final String SETK_RECORDING_ACTIVITY_TIME ="acimage:community:topics:recording:activityTime"; public static final String ZSETK_TOPIC_STAR_COUNT ="acimage:community:topics:rank:starCount"; public static final String ZSETK_TOPIC_PV ="acimage:community:topics:rank:pageView"; /** * key前缀,记录话题的最新变化时间(如新增评论等) */ public static final String ZSETK_TOPIC_ACTIVITY_TIME ="acimage:community:topics:rank:activityTime"; public static final String ZSETK_TOPIC_COMMENT_COUNT ="acimage:community:topics:rank:commentCount"; } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/global/enums/SortMode.java ================================================ package com.acimage.community.global.enums; import com.acimage.common.model.Index.TopicIndex; import com.acimage.common.utils.LambdaUtils; import org.elasticsearch.search.sort.FieldSortBuilder; import org.elasticsearch.search.sort.SortOrder; public enum SortMode { NORMAL, CREATE_TIME, ACTIVITY_TIME, STAR_COUNT, PAGE_VIEW, COMMENT_COUNT; public String toColumn() { switch (this) { case NORMAL: return null; case CREATE_TIME: return LambdaUtils.columnOf(TopicIndex::getCreateTime); case ACTIVITY_TIME: return LambdaUtils.columnOf(TopicIndex::getActivityTime); case STAR_COUNT: return LambdaUtils.columnOf(TopicIndex::getStarCount); case PAGE_VIEW: return LambdaUtils.columnOf(TopicIndex::getPageView); case COMMENT_COUNT: return LambdaUtils.columnOf(TopicIndex::getCommentCount); } return null; } public FieldSortBuilder toSortBuilder(){ if(this==NORMAL){ return null; } if(this.toColumn()==null){ return null; } return new FieldSortBuilder(this.toColumn()) .order(SortOrder.DESC); } public static FieldSortBuilder toSortBuilder(SortMode sortMode){ if(sortMode==null){ return null; } if(sortMode==NORMAL){ return null; } if(sortMode.toColumn()==null){ return null; } return new FieldSortBuilder(sortMode.toColumn()) .order(SortOrder.DESC); } } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/global/enums/TopicAttribute.java ================================================ package com.acimage.community.global.enums; import com.acimage.common.model.domain.community.Topic; import com.acimage.common.utils.LambdaUtils; import com.acimage.community.global.consts.TopicKeyConstants; import com.baomidou.mybatisplus.core.toolkit.support.SFunction; import java.util.*; public enum TopicAttribute { STAR_COUNT, PAGE_VIEW, COMMENT_COUNT, ACTIVITY_TIME; public static final Map toZSetKeyForRank; public static final Map toSetKey; public static final Map toKeyPrefix; public static final Map> toTopicField; static { toZSetKeyForRank = new HashMap() {{ put(STAR_COUNT, TopicKeyConstants.ZSETK_TOPIC_STAR_COUNT); put(PAGE_VIEW, TopicKeyConstants.ZSETK_TOPIC_PV); put(COMMENT_COUNT, TopicKeyConstants.ZSETK_TOPIC_COMMENT_COUNT); put(ACTIVITY_TIME, TopicKeyConstants.ZSETK_TOPIC_ACTIVITY_TIME); }}; toSetKey = new HashMap() {{ put(STAR_COUNT, TopicKeyConstants.SETK_RECORDING_STAR_COUNT_INCREMENT); put(PAGE_VIEW, TopicKeyConstants.SETK_RECORDING_PV_INCREMENT); put(COMMENT_COUNT, TopicKeyConstants.SETK_RECORDING_COMMENT_COUNT_INCREMENT); put(ACTIVITY_TIME, TopicKeyConstants.SETK_RECORDING_ACTIVITY_TIME); }}; toKeyPrefix = new HashMap() {{ put(STAR_COUNT, TopicKeyConstants.STRINGKP_TOPIC_STAR_COUNT_INCREMENT); put(PAGE_VIEW, TopicKeyConstants.LOGKP_TOPIC_PV); put(COMMENT_COUNT, TopicKeyConstants.STRINGKP_TOPIC_COMMENT_COUNT_INCREMENT); put(ACTIVITY_TIME, TopicKeyConstants.STRINGKP_TOPIC_ACTIVITY_TIME); }}; toTopicField = new HashMap>() {{ put(STAR_COUNT, Topic::getStarCount); put(PAGE_VIEW, Topic::getPageView); put(COMMENT_COUNT, Topic::getCommentCount); put(ACTIVITY_TIME, Topic::getActivityTime); }}; } public String zSetKey() { return toZSetKeyForRank.get(this); } public String setKeyForRecordingId() { return toSetKey.get(this); } public String keyPrefix() { return toKeyPrefix.get(this); } public String toUnderlineColumnName() { return LambdaUtils.underlineColumnNameOf(toTopicField.get(this)); } public String toFieldName() { return LambdaUtils.columnOf(toTopicField.get(this)); } public SFunction toGetFunction(){ return toTopicField.get(this); } } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/listener/CommentEventListener.java ================================================ package com.acimage.community.listener; import com.acimage.community.listener.event.CommentEvent; import org.springframework.context.ApplicationListener; import org.springframework.stereotype.Component; @Component public class CommentEventListener implements ApplicationListener { @Override public void onApplicationEvent(CommentEvent event) { //增加用户发表话题数 } } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/listener/PublishTopicEventListener.java ================================================ package com.acimage.community.listener; import com.acimage.community.listener.event.TopicEvent; import com.acimage.community.service.cmtyuser.CmtyUserWriteService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationListener; import org.springframework.stereotype.Component; @Component public class PublishTopicEventListener implements ApplicationListener { @Autowired CmtyUserWriteService cmtyUserWriteService; @Override public void onApplicationEvent(TopicEvent event) { //增加用户发表话题数 cmtyUserWriteService.updateTopicCountByIncrement(event.getUserId(),1); } } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/listener/StarEventListener.java ================================================ package com.acimage.community.listener; import com.acimage.community.listener.event.StarEvent; import com.acimage.community.service.cmtyuser.CmtyUserWriteService; import com.acimage.community.service.topic.TopicSpAttrWriteService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationListener; import org.springframework.stereotype.Component; @Component public class StarEventListener implements ApplicationListener { @Autowired CmtyUserWriteService cmtyUserWriteService; @Autowired TopicSpAttrWriteService topicSpAttrWriteService; @Override public void onApplicationEvent(StarEvent event) { } } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/listener/event/CommentEvent.java ================================================ package com.acimage.community.listener.event; import lombok.Getter; import lombok.Setter; import org.springframework.context.ApplicationEvent; @Getter @Setter public class CommentEvent extends ApplicationEvent { private Long userId; private Long topicId; public CommentEvent(Object source, Long userId, Long topicId) { super(source); this.userId = userId; this.topicId = topicId; } } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/listener/event/StarEvent.java ================================================ package com.acimage.community.listener.event; import lombok.Getter; import lombok.Setter; import org.springframework.context.ApplicationEvent; @Getter @Setter public class StarEvent extends ApplicationEvent { Long topicId; Long ownerId; Long fromUserId; public StarEvent(Object source, Long topicId, Long ownerId, Long fromUserId) { super(source); this.topicId = topicId; this.ownerId = ownerId; this.fromUserId = fromUserId; } } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/listener/event/TopicEvent.java ================================================ package com.acimage.community.listener.event; import lombok.Getter; import lombok.Setter; import org.springframework.context.ApplicationEvent; @Getter @Setter public class TopicEvent extends ApplicationEvent { private Long userId; private Long topicId; public TopicEvent(Object source, Long userId, Long topicId) { super(source); this.userId = userId; this.topicId = topicId; } } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/model/request/CommentAddReq.java ================================================ package com.acimage.community.model.request; import com.acimage.common.model.domain.community.Comment; import lombok.Data; import javax.validation.constraints.NotNull; import javax.validation.constraints.Positive; import javax.validation.constraints.Size; @Data public class CommentAddReq { @Positive @NotNull(message = "话题id不能为空") private Long topicId; @Size(min = Comment.CONTENT_MIN, max = Comment.CONTENT_MAX, message = Comment.CONTENT_VALIDATION_MSG) private String content; } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/model/request/CommentModifyReq.java ================================================ package com.acimage.community.model.request; import com.acimage.common.model.domain.community.Comment; import lombok.Data; import javax.validation.constraints.NotNull; import javax.validation.constraints.Positive; import javax.validation.constraints.Size; @Data public class CommentModifyReq { @Positive @NotNull(message = "id不能为空") Long id; @Size(min = Comment.CONTENT_MIN, max = Comment.CONTENT_MAX, message = Comment.CONTENT_VALIDATION_MSG) String content; } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/model/request/TopicAddReq.java ================================================ package com.acimage.community.model.request; import com.acimage.common.model.domain.community.Category; import com.acimage.common.model.domain.community.Topic; import com.acimage.common.model.domain.community.TopicHtml; import lombok.Data; import lombok.NoArgsConstructor; import javax.validation.constraints.NotNull; import javax.validation.constraints.Positive; import javax.validation.constraints.Size; @Data @NoArgsConstructor public class TopicAddReq { @Size(min = Topic.TITLE_MIN, max = Topic.TITLE_MAX, message = Topic.TITLE_VALIDATION_MSG) private String title; @Size(min = TopicHtml.HTML_MIN, max = TopicHtml.HTML_MAX, message = TopicHtml.HTML_VALIDATION_MSG) private String html; @Positive @NotNull Integer CategoryId; Integer[] tagIds; } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/model/request/TopicModifyHtmlReq.java ================================================ package com.acimage.community.model.request; import com.acimage.common.model.domain.community.TopicHtml; import lombok.Data; import lombok.NoArgsConstructor; import javax.validation.constraints.Positive; import javax.validation.constraints.Size; @Data @NoArgsConstructor public class TopicModifyHtmlReq { @Positive Long id; @Size(min = TopicHtml.HTML_MIN, max = TopicHtml.HTML_MAX, message = TopicHtml.HTML_VALIDATION_MSG) private String html; } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/model/request/TopicQueryByCategoryIdReq.java ================================================ package com.acimage.community.model.request; import com.acimage.community.global.enums.SortMode; import lombok.Data; import lombok.NoArgsConstructor; import org.hibernate.validator.constraints.Range; import javax.validation.constraints.NotNull; import javax.validation.constraints.Positive; @Data @NoArgsConstructor public class TopicQueryByCategoryIdReq { @Positive @NotNull private Integer categoryId; @Range(min=1,max=100,message = "页码在1到100之间") private Integer pageNo; @Range(min=4,max=20,message = "页大小在4到20之间") private Integer pageSize; private SortMode sortMode; } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/model/request/TopicQueryBySortReq.java ================================================ package com.acimage.community.model.request; import com.acimage.community.global.enums.SortMode; import lombok.Data; import lombok.NoArgsConstructor; import org.hibernate.validator.constraints.Range; import javax.validation.constraints.NotNull; @Data @NoArgsConstructor public class TopicQueryBySortReq { @Range(min=1,max=100,message = "页码在1到100之间") private Integer pageNo; @Range(min=4,max=20,message = "页大小在4到20之间") private Integer pageSize; @NotNull private SortMode sortMode; } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/model/request/TopicQueryByTagIdReq.java ================================================ package com.acimage.community.model.request; import com.acimage.community.global.enums.SortMode; import lombok.Data; import lombok.NoArgsConstructor; import javax.validation.constraints.Max; import javax.validation.constraints.Min; import javax.validation.constraints.NotNull; import javax.validation.constraints.Positive; @Data @NoArgsConstructor public class TopicQueryByTagIdReq { @Positive @NotNull private Integer tagId; @Min(1) @Max(50) private Integer pageNo; @Min(1) @Max(20) private Integer pageSize; private SortMode sortMode; } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/model/request/TopicSearchReq.java ================================================ package com.acimage.community.model.request; import com.acimage.community.global.enums.SortMode; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import javax.validation.constraints.Max; import javax.validation.constraints.NotNull; import javax.validation.constraints.Positive; import javax.validation.constraints.Size; @Data @NoArgsConstructor @AllArgsConstructor public class TopicSearchReq { private Integer categoryId; private Integer tagId; @Positive @NotNull @Max(30) private Integer pageNo; @Size(max = 15, message = "搜索字数不超过15") private String search; private SortMode sortMode; } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/model/request/UserLoginReq.java ================================================ package com.acimage.community.model.request; import lombok.Data; import javax.validation.constraints.Size; @Data public class UserLoginReq { @Size(min = 2, max = 12, message = "用户名长度在2到12之间") private String username; @Size(min = 100, max = 2000, message = "非法密码") String password; } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/model/request/UserRegisterReq.java ================================================ package com.acimage.community.model.request; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import javax.validation.constraints.*; @Data @NoArgsConstructor @AllArgsConstructor public class UserRegisterReq { @Size(min=2,max=12,message = "用户名长度在2到12之间") private String username; @Size(min=100,max=2000,message = "非法密码") String password; @Email(message = "邮箱格式错误") @Size(min=6,max=32,message = "邮箱长度在6到32之间") private String email; } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/model/vo/TopicInfoVo.java ================================================ package com.acimage.community.model.vo; import com.acimage.common.model.domain.community.Comment; import com.acimage.common.model.domain.community.Topic; import com.acimage.common.model.domain.user.User; import lombok.Data; import lombok.NoArgsConstructor; import java.util.Date; import java.util.List; @Data @NoArgsConstructor public class TopicInfoVo { private Long id; private Long userId; private String content; private String title; private Integer starCount; private Integer pageView; private Integer commentCount; private String coverImageUrl; private Integer categoryId; private List tagIds; private Date activityTime; private Date createTime; private Date updateTime; String html; User user; List comments; List similarTopics; } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/mq/config/SyncEsMqConfig.java ================================================ package com.acimage.community.mq.config; import com.acimage.common.global.consts.MqConstants; import org.springframework.amqp.core.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class SyncEsMqConfig { @Autowired AmqpAdmin rabbitAdmin; @Bean public Queue syncEsQueue() { // durable:是否持久化,默认是false,持久化队列:会被存储在磁盘上,当消息代理重启时仍然存在,暂存队列:当前连接有效 // exclusive:默认也是false,只能被当前创建的连接使用,而且当连接关闭后队列即被删除。此参考优先级高于durable // autoDelete:是否自动删除,当没有生产者或者消费者使用此队列,该队列会自动删除。 // return new Queue("TestDirectQueue",true,true,false); //一般设置一下队列的持久化就好,其余两个就是默认false return new Queue(MqConstants.SYNC_ES_QUEUE, true); } @Bean DirectExchange syncEsExchange() { return new DirectExchange(MqConstants.SYNC_ES_EXCHANGE, true, false); } //绑定 将队列和交换机绑定, 并设置用于匹配键 @Bean Binding bindingSyncEs() { return BindingBuilder.bind(syncEsQueue()).to(syncEsExchange()).with(MqConstants.SYNC_ES_ROUTE); } //创建交换机和队列 @Bean public void createExchangeQueueForSyncEs() { rabbitAdmin.declareExchange(syncEsExchange()); rabbitAdmin.declareQueue(syncEsQueue()); } } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/mq/config/SyncImagesMqConfig.java ================================================ package com.acimage.community.mq.config; import com.acimage.common.global.consts.MqConstants; import org.springframework.amqp.core.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class SyncImagesMqConfig { @Autowired AmqpAdmin rabbitAdmin; @Bean public Queue syncImagesQueue() { // durable:是否持久化,默认是false,持久化队列:会被存储在磁盘上,当消息代理重启时仍然存在,暂存队列:当前连接有效 // exclusive:默认也是false,只能被当前创建的连接使用,而且当连接关闭后队列即被删除。此参考优先级高于durable // autoDelete:是否自动删除,当没有生产者或者消费者使用此队列,该队列会自动删除。 // return new Queue("TestDirectQueue",true,true,false); //一般设置一下队列的持久化就好,其余两个就是默认false return new Queue(MqConstants.SYNC_IMAGES_QUEUE, true); } @Bean DirectExchange topicImagesExchange() { return new DirectExchange(MqConstants.TOPIC_IMAGES_EXCHANGE, true, false); } //绑定 将队列和交换机绑定, 并设置用于匹配键 @Bean Binding bindingSyncImages() { return BindingBuilder.bind(syncImagesQueue()).to(topicImagesExchange()).with(MqConstants.SYNC_IMAGES_ROUTE); } //创建交换机和队列 @Bean public void createExchangeQueueForSyncImages() { rabbitAdmin.declareExchange(topicImagesExchange()); rabbitAdmin.declareQueue(syncImagesQueue()); } } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/mq/config/UserMsgMqConfig.java ================================================ package com.acimage.community.mq.config; import com.acimage.common.global.consts.MqConstants; import org.springframework.amqp.core.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class UserMsgMqConfig { @Autowired AmqpAdmin rabbitAdmin; @Bean public Queue userMsgQueue() { return new Queue(MqConstants.USER_MSG_QUEUE, true); } @Bean DirectExchange communityUserExchange() { return new DirectExchange(MqConstants.COMMUNITY_USER_EXCHANGE, true, false); } //绑定 将队列和交换机绑定, 并设置用于匹配键 @Bean Binding bindingUserMsg() { return BindingBuilder.bind(userMsgQueue()).to(communityUserExchange()).with(MqConstants.USER_MSG_ROUTE); } //创建交换机和队列 @Bean public void createExchangeQueueForUserMsg() { rabbitAdmin.declareExchange(communityUserExchange()); rabbitAdmin.declareQueue(userMsgQueue()); } } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/mq/consumer/SyncEsConsumer.java ================================================ package com.acimage.community.mq.consumer; import com.acimage.common.global.consts.EsConstants; import com.acimage.common.global.consts.MqConstants; import com.acimage.common.model.mq.dto.EsAddDto; import com.acimage.common.model.mq.dto.EsDeleteDto; import com.acimage.common.model.mq.dto.EsUpdateByIdDto; import com.acimage.common.model.mq.dto.EsUpdateByTermDto; import com.acimage.common.utils.EsUtils; import com.acimage.common.utils.ExceptionUtils; import com.rabbitmq.client.Channel; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.core.Message; import org.springframework.amqp.rabbit.annotation.RabbitHandler; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.io.IOException; @Slf4j @Component @RabbitListener(queues = MqConstants.SYNC_ES_QUEUE) public class SyncEsConsumer { @Autowired EsUtils esUtils; @RabbitHandler public void syncAdd(Channel channel, Message message, EsAddDto esAddDto) { log.info("同步es数据:{}", esAddDto); try { esUtils.save(esAddDto); } catch (Exception e) { ExceptionUtils.printIfDev(e); log.error("同步es数据失败 error:{} data:{}", e.getMessage(), esAddDto); } finally { String messageBody = new String(message.getBody()); try { channel.basicAck(message.getMessageProperties().getDeliveryTag(), false); } catch (IOException e) { ExceptionUtils.printIfDev(e); log.error("同步es数据ack失败 error:{} message:{}", e.getMessage(), messageBody); try { channel.basicReject(message.getMessageProperties().getDeliveryTag(), false); } catch (IOException ex) { ExceptionUtils.printIfDev(ex); log.error("同步es数据reject失败 error:{} message:{}", ex.getMessage(), messageBody); } } } } @RabbitHandler public void syncDelete(Channel channel, Message message, EsDeleteDto esDeleteDto) { log.info("同步es数据:{}", esDeleteDto); try { esUtils.remove(esDeleteDto); } catch (Exception e) { ExceptionUtils.printIfDev(e); log.error("同步es数据失败 error:{} data:{}", e.getMessage(), esDeleteDto); } finally { String messageBody = new String(message.getBody()); try { channel.basicAck(message.getMessageProperties().getDeliveryTag(), false); } catch (IOException e) { ExceptionUtils.printIfDev(e); log.error("同步es数据ack失败 error:{} message:{}", e.getMessage(), messageBody); try { channel.basicReject(message.getMessageProperties().getDeliveryTag(), false); } catch (IOException ex) { ExceptionUtils.printIfDev(ex); log.error("同步es数据reject失败 error:{} message:{}", ex.getMessage(), messageBody); } } } } @RabbitHandler public void syncUpdate(Channel channel, Message message, EsUpdateByIdDto esUpdateDto) { log.info("同步es数据:{}", esUpdateDto); try { esUtils.updateById(esUpdateDto); } catch (Exception e) { ExceptionUtils.printIfDev(e); log.error("同步es数据失败 error:{} data:{}", e.getMessage(), esUpdateDto); } finally { String messageBody = new String(message.getBody()); try { channel.basicAck(message.getMessageProperties().getDeliveryTag(), false); } catch (IOException e) { ExceptionUtils.printIfDev(e); log.error("同步es数据ack失败 error:{} message:{}", e.getMessage(), messageBody); try { channel.basicReject(message.getMessageProperties().getDeliveryTag(), false); } catch (IOException ex) { ExceptionUtils.printIfDev(ex); log.error("同步es数据reject失败 error:{} message:{}", ex.getMessage(), messageBody); } } } } // @RabbitHandler // public void syncBatchUpdate(Channel channel, Message message, EsUpdateByTermDto updateDto) { // log.info("同步es数据:{}", updateDto); // try { // esUtils.UpdateByTerm(updateDto); // // } catch (Exception e) { // e.printStackTrace(); // log.error("同步es数据失败 error:{} data:{}", e.getMessage(), updateDto); // // } finally { // String messageBody = new String(message.getBody()); // try { // channel.basicAck(message.getMessageProperties().getDeliveryTag(), false); // } catch (IOException e) { // e.printStackTrace(); // log.error("同步es数据ack失败 error:{} message:{}", e.getMessage(), messageBody); // try { // channel.basicReject(message.getMessageProperties().getDeliveryTag(), false); // } catch (IOException ex) { // ex.printStackTrace(); // log.error("同步es数据reject失败 error:{} message:{}", ex.getMessage(), messageBody); // } // } // } // } } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/mq/consumer/SyncUserConsumer.java ================================================ package com.acimage.community.mq.consumer; import com.acimage.common.global.consts.MqConstants; import com.acimage.common.model.Index.TopicIndex; import com.acimage.common.model.domain.community.CmtyUser; import com.acimage.common.model.mq.dto.EsUpdateByTermDto; import com.acimage.common.model.mq.dto.UserIdWithPhotoUrl; import com.acimage.common.model.mq.dto.UserIdWithUsername; import com.acimage.common.utils.EsUtils; import com.acimage.common.utils.ExceptionUtils; import com.acimage.common.utils.LambdaUtils; import com.acimage.common.utils.SpringContextUtils; import com.acimage.community.service.cmtyuser.CmtyUserWriteService; import com.rabbitmq.client.Channel; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.core.Message; import org.springframework.amqp.rabbit.annotation.RabbitHandler; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.io.IOException; import java.util.List; @Slf4j @Component @RabbitListener(queues = MqConstants.SYNC_USER_QUEUE) public class SyncUserConsumer { @Autowired CmtyUserWriteService cmtyUserWriteService; @Autowired EsUtils esUtils; @RabbitHandler public void syncUsername(Channel channel, Message message, UserIdWithUsername userIdWithUsername) { log.info("同步用户名:{}", userIdWithUsername); try { cmtyUserWriteService.updateUsername(userIdWithUsername.getUserId(), userIdWithUsername.getUsername()); TopicIndex topicIndex = TopicIndex.builder() .userId(userIdWithUsername.getUserId()) .username(userIdWithUsername.getUsername()) .build(); EsUpdateByTermDto updateDto = new EsUpdateByTermDto(); updateDto.with(topicIndex); String termColumn = LambdaUtils.columnOf(TopicIndex::getUserId); List columns = LambdaUtils.columnsFrom(TopicIndex::getUsername); updateDto.setTermColumn(termColumn); updateDto.setColumns(columns); esUtils.UpdateByTerm(updateDto); } catch (Exception e) { ExceptionUtils.printIfDev(e); log.error("同步用户名任务失败 error:{} 对象:{}", e.getMessage(), userIdWithUsername); } finally { String messageBody = new String(message.getBody()); try { channel.basicAck(message.getMessageProperties().getDeliveryTag(), false); } catch (IOException e) { ExceptionUtils.printIfDev(e); log.error("同步用户名ack失败 error:{} message:{}", e.getMessage(), messageBody); try { channel.basicReject(message.getMessageProperties().getDeliveryTag(), false); } catch (IOException ex) { ExceptionUtils.printIfDev(e); log.error("同步用户名reject失败 error:{} message:{}", ex.getMessage(), messageBody); } } } } @RabbitHandler public void syncPhotoUrl(Channel channel, Message message, UserIdWithPhotoUrl userIdWithPhotoUrl) { log.info("同步头像地址:{}", userIdWithPhotoUrl); try { cmtyUserWriteService.updatePhotoUrl(userIdWithPhotoUrl.getUserId(), userIdWithPhotoUrl.getPhotoUrl()); TopicIndex topicIndex = TopicIndex.builder() .userId(userIdWithPhotoUrl.getUserId()) .photoUrl(userIdWithPhotoUrl.getPhotoUrl()) .build(); EsUpdateByTermDto updateDto = new EsUpdateByTermDto(); updateDto.with(topicIndex); String termColumn = LambdaUtils.columnOf(TopicIndex::getUserId); List columns = LambdaUtils.columnsFrom(TopicIndex::getPhotoUrl); updateDto.setTermColumn(termColumn); updateDto.setColumns(columns); esUtils.UpdateByTerm(updateDto); } catch (Exception e) { ExceptionUtils.printIfDev(e); log.error("同步头像地址任务失败 error:{} data:{}", e.getMessage(), userIdWithPhotoUrl); } finally { String messageBody = new String(message.getBody()); try { channel.basicAck(message.getMessageProperties().getDeliveryTag(), false); } catch (IOException e) { ExceptionUtils.printIfDev(e); log.error("同步头像地址ack失败 error:{} message:{}", e.getMessage(), messageBody); try { channel.basicReject(message.getMessageProperties().getDeliveryTag(), false); } catch (IOException ex) { ExceptionUtils.printIfDev(e); log.error("同步头像地址reject失败 error:{} message:{}", ex.getMessage(), messageBody); } } } } @RabbitHandler public void addUser(Channel channel, Message message, CmtyUser cmtyUser) { log.info("新增用户:{}", cmtyUser); try { cmtyUserWriteService.save(cmtyUser); } catch (Exception e) { ExceptionUtils.printIfDev(e); log.error("新增用户 error:{} 对象:{}", e.getMessage(), cmtyUser); } finally { String messageBody = new String(message.getBody()); try { channel.basicAck(message.getMessageProperties().getDeliveryTag(), false); } catch (IOException e) { ExceptionUtils.printIfDev(e); log.error("新增用户ack失败 error:{} message:{}", e.getMessage(), messageBody); try { channel.basicReject(message.getMessageProperties().getDeliveryTag(), false); } catch (IOException ex) { ExceptionUtils.printIfDev(e); log.error("新增用户reject失败 error:{} message:{}", ex.getMessage(), messageBody); } } } } } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/mq/producer/SyncEsMqProducer.java ================================================ package com.acimage.community.mq.producer; import com.acimage.common.global.consts.MqConstants; import com.acimage.common.model.mq.dto.EsAddDto; import com.acimage.common.model.mq.dto.EsDeleteDto; import com.acimage.common.model.mq.dto.EsUpdateByIdDto; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.util.List; @Component public class SyncEsMqProducer { @Autowired RabbitTemplate rabbitTemplate; public void sendAddMessage(Object entity) { EsAddDto esAddDto = new EsAddDto(entity); rabbitTemplate.convertAndSend(MqConstants.SYNC_ES_EXCHANGE, MqConstants.SYNC_ES_ROUTE, esAddDto); } public void sendUpdateMessage(Object entity, List columns) { EsUpdateByIdDto esUpdateDto = new EsUpdateByIdDto(); esUpdateDto.with(entity); esUpdateDto.setColumns(columns); rabbitTemplate.convertAndSend(MqConstants.SYNC_ES_EXCHANGE, MqConstants.SYNC_ES_ROUTE, esUpdateDto); } public void sendDeleteMessage(String id,Class type) { EsDeleteDto esDeleteDto=new EsDeleteDto(id,type); rabbitTemplate.convertAndSend(MqConstants.SYNC_ES_EXCHANGE, MqConstants.SYNC_ES_ROUTE, esDeleteDto); } } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/mq/producer/UserMsgMqProducer.java ================================================ package com.acimage.community.mq.producer; import com.acimage.common.global.consts.MqConstants; import com.acimage.common.model.domain.user.CommentMsg; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component public class UserMsgMqProducer { @Autowired RabbitTemplate rabbitTemplate; public void sendCommentMessage(CommentMsg commentMsg) { rabbitTemplate.convertAndSend(MqConstants.COMMUNITY_USER_EXCHANGE, MqConstants.USER_MSG_ROUTE, commentMsg); } } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/mq/producer/syncImagesMqProducer.java ================================================ package com.acimage.community.mq.producer; import cn.hutool.core.collection.CollectionUtil; import com.acimage.common.global.consts.MqConstants; import com.acimage.common.model.mq.dto.SyncImagesUpdateDto; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component public class syncImagesMqProducer { @Autowired RabbitTemplate rabbitTemplate; // public void sendUploadImageMessage(long imageId, String url) { // ImageIdWithUrl imageIdWithUrl = new ImageIdWithUrl(imageId, url); // rabbitTemplate.convertAndSend(MqConstants.TOPIC_IMAGES_EXCHANGE, MqConstants.HASH_IMAGE_ROUTE, imageIdWithUrl); // } // // public void sendHashImagesMessage(long topicId) { // rabbitTemplate.convertAndSend(MqConstants.TOPIC_IMAGES_EXCHANGE, MqConstants.HASH_IMAGE_ROUTE, topicId); // } public void sendSyncImagesMessage(SyncImagesUpdateDto updateDto) { if (!CollectionUtil.isEmpty(updateDto.getAddImageUrls()) || !CollectionUtil.isEmpty(updateDto.getRemoveImageUrls())) { rabbitTemplate.convertAndSend(MqConstants.TOPIC_IMAGES_EXCHANGE, MqConstants.SYNC_IMAGES_ROUTE, updateDto); } } // public void sendRemoveTopicMessage(long topicId){ // // rabbitTemplate.convertAndSend(MqConstants.TOPIC_IMAGES_EXCHANGE, // MqConstants.REMOVE_TOPIC_IMAGE_ROUTE, topicId); // } } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/runner/CreateIndexRunner.java ================================================ package com.acimage.community.runner; import com.acimage.common.model.Index.TopicIndex; import com.acimage.common.utils.EsUtils; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; import org.springframework.stereotype.Component; @Slf4j @Component public class CreateIndexRunner implements ApplicationRunner { @Autowired EsUtils esUtils; @Override public void run(ApplicationArguments args) { esUtils.createIndexIfNotExist(TopicIndex.class); } } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/runner/PreheatApplicationRunner.java ================================================ package com.acimage.community.runner; import com.acimage.community.service.topic.TopicPreheatService; import com.acimage.community.global.enums.TopicAttribute; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; import org.springframework.stereotype.Component; import java.util.concurrent.TimeUnit; @Slf4j @Component public class PreheatApplicationRunner implements ApplicationRunner { @Autowired TopicPreheatService topicPreheatService; @Override public void run(ApplicationArguments args) { log.info("start 预热热点topic"); int initialRankSize = 30; int cacheSize = 10; long expireSeconds = 3L; for (TopicAttribute attr : TopicAttribute.values()) { log.info("start 根据{}预热", attr); int size=initialRankSize; if(attr==TopicAttribute.ACTIVITY_TIME){ size=1000; } topicPreheatService.preheatTopicsOrderBy(attr, size, cacheSize, expireSeconds, TimeUnit.SECONDS); } log.info("end 预热热点topic"); } } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/schedule/UpdateActivityTimeJob.java ================================================ package com.acimage.community.schedule; import cn.hutool.core.collection.CollectionUtil; import com.acimage.common.utils.redis.RedisUtils; import com.acimage.community.service.topic.TopicSpAttrWriteService; import com.acimage.community.global.consts.TopicKeyConstants; import lombok.extern.slf4j.Slf4j; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.quartz.QuartzJobBean; import org.springframework.stereotype.Component; import java.util.ArrayList; import java.util.Date; import java.util.List; /** * 不再定时任务更新活跃时间,直接在增加评论或修改话题的时候直接更新 */ @Deprecated @Slf4j public class UpdateActivityTimeJob extends QuartzJobBean { @Autowired RedisUtils redisUtils; @Autowired TopicSpAttrWriteService topicSpAttrWriteService; /** * 从redis中获取话题的新增浏览量并写入到数据库中 */ @Override protected void executeInternal(JobExecutionContext context) throws JobExecutionException { //批量更新到数据库的大小 final int BATCH_SIZE = 10; log.info("start 系统定时任务:保存活跃时间"); //获取哪些话题评论数有变化 List topicIdList = redisUtils.membersForSet(TopicKeyConstants.SETK_RECORDING_ACTIVITY_TIME, Long.class); if (CollectionUtil.isEmpty(topicIdList)) { return; } StringBuilder logString = new StringBuilder(); int index = 0; //获取话题id,评论数增量,相应redis的key List activityTimeKeys = new ArrayList<>(BATCH_SIZE); List batchTopicIds = new ArrayList<>(BATCH_SIZE); List batchActivityTime = new ArrayList<>(BATCH_SIZE); for (Long topicId : topicIdList) { index++; String activityTimeKey = TopicKeyConstants.STRINGKP_TOPIC_ACTIVITY_TIME + topicId; //获取活跃时间 Date activityTime = redisUtils.getObjectFromString(activityTimeKey, Date.class); if (activityTime != null) { batchTopicIds.add(topicId); batchActivityTime.add(activityTime); } activityTimeKeys.add(activityTimeKey); //日志记录浏览量增加的信息 logString.append(String.format("%s活跃时间更新为%s ", topicId, activityTime)); if (index % BATCH_SIZE == 0 || index == topicIdList.size()) { //数据库批量增加浏览量 try { topicSpAttrWriteService.updateBatchActivityTime(batchTopicIds, batchActivityTime); } catch (Exception e) { log.error("更新activityTime失败id:{} activityTime:{}",batchTopicIds,batchActivityTime); } //批量移除对应值或删除对应键值,这两者顺序不可交换! redisUtils.removeForSet(TopicKeyConstants.SETK_RECORDING_ACTIVITY_TIME, batchTopicIds); redisUtils.delete(activityTimeKeys); log.info(logString.toString()); //清空 batchTopicIds.clear(); batchActivityTime.clear(); activityTimeKeys.clear(); //重新初始化 index=0; logString = new StringBuilder(); } } log.info("end 系统定时任务:保存活跃时间"); } } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/schedule/UpdateActivityTimeJobConfig.java ================================================ package com.acimage.community.schedule; import org.quartz.*; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Deprecated public class UpdateActivityTimeJobConfig { private static String cron ="0 */5 * * * ?"; @Bean public JobDetail updateActivityTimeJobDetail() { JobDetail jobDetail = JobBuilder.newJob(UpdateActivityTimeJob.class) .withIdentity("updateActivityTime", "topicGroup") .storeDurably() .build(); return jobDetail; } @Bean public Trigger updateActivityTimeJobTrigger() { Trigger trigger = TriggerBuilder.newTrigger() .forJob(updateActivityTimeJobDetail()) .withIdentity("updateActivityTimeTrigger", "topicTrigger") .startNow() .withSchedule(CronScheduleBuilder.cronSchedule(cron)) .build(); return trigger; } } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/schedule/UpdateCommentCountJob.java ================================================ package com.acimage.community.schedule; import cn.hutool.core.collection.CollectionUtil; import com.acimage.common.model.domain.community.Topic; import com.acimage.common.utils.LambdaUtils; import com.acimage.common.utils.redis.RedisUtils; import com.acimage.community.service.topic.TopicSpAttrWriteService; import com.acimage.community.global.consts.TopicKeyConstants; import lombok.extern.slf4j.Slf4j; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.quartz.QuartzJobBean; import java.util.ArrayList; import java.util.List; @Deprecated @Slf4j public class UpdateCommentCountJob extends QuartzJobBean { @Autowired RedisUtils redisUtils; @Autowired TopicSpAttrWriteService topicSpAttrWriteService; @Override protected void executeInternal(JobExecutionContext context) throws JobExecutionException { //批量更新到数据库的大小 final int BATCH_SIZE = 10; log.info("start 系统定时任务:保存评论数变化"); //获取哪些话题评论数有变化 List topicIdList = redisUtils.membersForSet(TopicKeyConstants.SETK_RECORDING_COMMENT_COUNT_INCREMENT, Long.class); if (CollectionUtil.isEmpty(topicIdList)) { return; } StringBuilder logString = new StringBuilder(); int index = 0; List batchTopicIds = new ArrayList<>(BATCH_SIZE); List batchCcIncrements = new ArrayList<>(BATCH_SIZE); for (Long topicId : topicIdList) { index++; String ccIncrementKey = TopicKeyConstants.STRINGKP_TOPIC_COMMENT_COUNT_INCREMENT + topicId; String hashKeyForTopic= TopicKeyConstants.HASHKP_TOPIC+topicId; String fieldName=LambdaUtils.columnOf(Topic::getCommentCount); Long ccIncrement = redisUtils.getAndCombineAndDelete(ccIncrementKey, hashKeyForTopic, fieldName); //记录话题id,评论数增量,相应redis的key、value if (ccIncrement != null) { batchTopicIds.add(topicId); batchCcIncrements.add(ccIncrement.intValue()); }else { redisUtils.removeForSet(TopicKeyConstants.SETK_RECORDING_COMMENT_COUNT_INCREMENT, Long.toString(topicId)); } //日志记录变化量 logString.append(String.format("%s评论变化量为%d ", topicId, ccIncrement)); if (index % BATCH_SIZE == 0 || index == topicIdList.size()) { //数据库批量增加浏览量 try { topicSpAttrWriteService.updateCommentCountByIncrement(batchTopicIds, batchCcIncrements); } catch (Exception e) { log.error("error:更新commentCount变化失败 ids:{} increments:{}",batchTopicIds,batchCcIncrements); } //批量移除对应值或删除对应键值,这两者顺序不可交换! redisUtils.removeForSet(TopicKeyConstants.SETK_RECORDING_COMMENT_COUNT_INCREMENT, batchTopicIds); log.debug(logString.toString()); //清空 batchTopicIds.clear(); batchCcIncrements.clear(); //重新初始化 index=0; logString = new StringBuilder(); } } log.info("end 系统定时任务:保存评论数变化"); } } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/schedule/UpdateCommentCountJobConfig.java ================================================ package com.acimage.community.schedule; import org.quartz.*; import org.springframework.context.annotation.Bean; @Deprecated public class UpdateCommentCountJobConfig { private String cron ="0 */3 * * * ?"; @Bean public JobDetail updateCommentCountJobDetail() { JobDetail jobDetail = JobBuilder.newJob(UpdateCommentCountJob.class) .withIdentity("updateCommentCount", "topicGroup") .storeDurably() .build(); return jobDetail; } @Bean public Trigger updateCommentCountJobTrigger() { Trigger trigger = TriggerBuilder.newTrigger() .forJob(updateCommentCountJobDetail()) .withIdentity("updateCommentCountTrigger", "topicTrigger") .startNow() .withSchedule(CronScheduleBuilder.cronSchedule(cron)) .build(); return trigger; } } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/schedule/UpdatePageViewJob.java ================================================ package com.acimage.community.schedule; import cn.hutool.core.collection.CollectionUtil; import com.acimage.common.model.Index.TopicIndex; import com.acimage.common.model.domain.community.Topic; import com.acimage.common.utils.EsUtils; import com.acimage.common.utils.LambdaUtils; import com.acimage.common.utils.common.ListUtils; import com.acimage.common.utils.redis.RedisUtils; import com.acimage.community.service.topic.TopicQueryService; import com.acimage.community.service.topic.TopicSpAttrWriteService; import com.acimage.community.global.consts.TopicKeyConstants; import lombok.extern.slf4j.Slf4j; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.quartz.QuartzJobBean; import org.springframework.stereotype.Component; import java.util.*; import java.util.stream.Collectors; @Component @Slf4j public class UpdatePageViewJob extends QuartzJobBean { private final long FIXED_RATE_MINUTES = 57L; @Autowired RedisUtils redisUtils; @Autowired TopicSpAttrWriteService topicSpAttrWriteService; @Autowired TopicQueryService topicQueryService; @Autowired EsUtils esUtils; /** * 从redis中获取话题的新增浏览量并写入到数据库中 */ @Override protected void executeInternal(JobExecutionContext context) throws JobExecutionException { //批量更新大小 final int BATCH_SIZE = 10; log.info("start 系统定时任务:保存浏览量变化"); //获取哪些话题被记录了浏览量 List topicIdList = redisUtils.membersForSet(TopicKeyConstants.SETK_RECORDING_PV_INCREMENT, Long.class); if (CollectionUtil.isEmpty(topicIdList)) { return; } StringBuilder logString = new StringBuilder(); int index = 0; List batchPvLogKeys = new ArrayList<>(BATCH_SIZE); List batchTopicIds = new ArrayList<>(BATCH_SIZE); List batchPvIncrements = new ArrayList<>(BATCH_SIZE); for (Long topicId : topicIdList) { index++; String pvLogKey = TopicKeyConstants.LOGKP_TOPIC_PV + topicId; Long pvIncrement = redisUtils.sizeForHyperLogLog(pvLogKey); //记录话题id,浏览量增量,相应redis的key、value if (pvIncrement != null) { batchTopicIds.add(topicId); batchPvIncrements.add(pvIncrement.intValue()); } else { redisUtils.removeForSet(TopicKeyConstants.SETK_RECORDING_PV_INCREMENT,Long.toString(topicId) ); } batchPvLogKeys.add(pvLogKey); //日志记录浏览量增加的信息 logString.append(String.format("%s增加浏览量%d ", topicId, pvIncrement)); if (index % BATCH_SIZE == 0 || index == topicIdList.size()) { //数据库批量增加浏览量 try { topicSpAttrWriteService.updatePageViewByIncrement(batchTopicIds, batchPvIncrements); } catch (Exception e) { log.error("error:数据库批量更新pageView变化失败 ids:{} increments:{}",batchTopicIds,batchPvIncrements); } List topicList=topicQueryService.listTopicsByIds(batchTopicIds); List topicIndexList= topicList.stream().map(TopicIndex::from).collect(Collectors.toList()); List columns= LambdaUtils.columnsFrom(TopicIndex::getPageView); try { esUtils.batchUpdateById(topicIndexList,columns); } catch (Exception e) { List pageViews= ListUtils.extract(Topic::getPageView,topicList); log.error("error:es批量更新pageView ids:{} pvs:{}",batchTopicIds,pageViews); } //批量移除对应值或删除对应键值,这两者顺序不可交换! redisUtils.removeForSet(TopicKeyConstants.SETK_RECORDING_PV_INCREMENT, batchTopicIds); redisUtils.delete(batchPvLogKeys); //清空 batchTopicIds.clear(); batchPvIncrements.clear(); batchPvLogKeys.clear(); //重新初始化 index = 0; log.info(logString.toString()); logString = new StringBuilder(); } } log.info("end 系统定时任务:保存浏览量变化"); } } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/schedule/UpdatePageViewJobConfig.java ================================================ package com.acimage.community.schedule; import org.quartz.*; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class UpdatePageViewJobConfig { @Value("${cron.page-view-job}") private String cron ; @Bean public JobDetail updatePageViewJobDetail() { JobDetail jobDetail = JobBuilder.newJob(UpdatePageViewJob.class) .withIdentity("updatePageView", "topicGroup") .storeDurably() .build(); return jobDetail; } @Bean public Trigger updatePageViewJobTrigger() { Trigger trigger = TriggerBuilder.newTrigger() .forJob(updatePageViewJobDetail()) .withIdentity("updatePageViewTrigger", "topicTrigger") .withSchedule(CronScheduleBuilder.cronSchedule(cron)) .startNow() .build(); return trigger; } } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/schedule/UpdateStarCountJob.java ================================================ package com.acimage.community.schedule; import cn.hutool.core.collection.CollectionUtil; import com.acimage.common.model.domain.community.Topic; import com.acimage.common.utils.LambdaUtils; import com.acimage.common.utils.redis.RedisUtils; import com.acimage.community.service.topic.TopicSpAttrWriteService; import com.acimage.community.global.consts.TopicKeyConstants; import lombok.extern.slf4j.Slf4j; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.quartz.QuartzJobBean; import java.util.ArrayList; import java.util.List; @Deprecated @Slf4j public class UpdateStarCountJob extends QuartzJobBean { private final long FIXED_RATE_MINUTES = 11L; @Autowired RedisUtils redisUtils; @Autowired TopicSpAttrWriteService topicSpAttrWriteService; /** * 从redis中获取话题的新增浏览量并写入到数据库中 */ @Override protected void executeInternal(JobExecutionContext context) throws JobExecutionException { //批量更新到数据库的大小 final int BATCH_SIZE = 10; log.info("START 系统定时任务:保存收藏数变化"); //获取哪些话题评论数有变化 List topicIdList = redisUtils.membersForSet(TopicKeyConstants.SETK_RECORDING_STAR_COUNT_INCREMENT, Long.class); if (CollectionUtil.isEmpty(topicIdList)) { return; } StringBuilder logString = new StringBuilder(); int index = 0; List batchTopicIds = new ArrayList<>(BATCH_SIZE); List batchScIncrements = new ArrayList<>(BATCH_SIZE); for (Long topicId : topicIdList) { index++; String scIncrementKey = TopicKeyConstants.STRINGKP_TOPIC_STAR_COUNT_INCREMENT + topicId; String hashKeyForTopic= TopicKeyConstants.HASHKP_TOPIC+topicId; String fieldName= LambdaUtils.columnOf(Topic::getStarCount); Long scIncrement = redisUtils.getAndCombineAndDelete(scIncrementKey, hashKeyForTopic, fieldName); //记录话题id,评论数增量,相应redis的key、value if (scIncrement != null) { batchTopicIds.add(topicId); batchScIncrements.add(scIncrement.intValue()); }else{ redisUtils.removeForSet(TopicKeyConstants.SETK_RECORDING_STAR_COUNT_INCREMENT,Long.toString(topicId)); } //日志记录浏览量增加的信息 logString.append(String.format("%s收藏变化量为%d ", topicId, scIncrement)); if (index % BATCH_SIZE == 0 || index == topicIdList.size()) { //数据库批量增加浏览量 try { topicSpAttrWriteService.updateStarCountByIncrement(batchTopicIds, batchScIncrements); } catch (Exception e) { log.error("error:更新starCount变化失败 ids:{} increments:{}",batchTopicIds,batchScIncrements); } //批量移除对应值或删除对应键值,这两者顺序不可交换! redisUtils.removeForSet(TopicKeyConstants.SETK_RECORDING_STAR_COUNT_INCREMENT, batchTopicIds); log.info(logString.toString()); //清空 batchTopicIds.clear(); batchScIncrements.clear(); //重新初始化 index=0; logString = new StringBuilder(); } } log.info("END 系统定时任务:保存收藏数变化"); } } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/schedule/UpdateStarCountJobConfig.java ================================================ package com.acimage.community.schedule; import org.quartz.*; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Deprecated public class UpdateStarCountJobConfig { //3分钟 private String cron ="0 */3 * * * ?"; @Bean public JobDetail updateStarCountJobDetail() { JobDetail jobDetail = JobBuilder.newJob(UpdateStarCountJob.class) .withIdentity("updateStarCount", "topicGroup") .storeDurably() .build(); return jobDetail; } @Bean public Trigger updateStarCountJobTrigger() { Trigger trigger = TriggerBuilder.newTrigger() .forJob(updateStarCountJobDetail()) .withIdentity("updateStarCountTrigger", "topicTrigger") .startNow() .withSchedule(CronScheduleBuilder.cronSchedule(cron)) .build(); return trigger; } } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/service/categoty/CategoryQueryService.java ================================================ package com.acimage.community.service.categoty; import com.acimage.common.model.domain.community.Category; import java.util.List; public interface CategoryQueryService { List listAll(); } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/service/categoty/impl/CategoryQueryServiceImpl.java ================================================ package com.acimage.community.service.categoty.impl; import com.acimage.common.model.domain.community.Category; import com.acimage.community.dao.CategoryDao; import com.acimage.community.service.categoty.CategoryQueryService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; @Service public class CategoryQueryServiceImpl implements CategoryQueryService { @Autowired CategoryDao categoryDao; @Override public List listAll(){ return categoryDao.selectList(null); } } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/service/cmtyuser/CmtyUserQueryService.java ================================================ package com.acimage.community.service.cmtyuser; import com.acimage.common.model.domain.community.CmtyUser; import com.acimage.common.redis.annotation.QueryRedis; import com.acimage.common.redis.enums.DataType; import com.acimage.community.depreted.userstatistic.consts.KeyConstants; public interface CmtyUserQueryService { @QueryRedis(keyPrefix = KeyConstants.STRINGKP_CMTY_USER, expire = 10L, dataType = DataType.HASH) CmtyUser getCmtyUser(long userId); } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/service/cmtyuser/CmtyUserRankService.java ================================================ package com.acimage.community.service.cmtyuser; import com.acimage.common.model.domain.community.CmtyUser; import com.acimage.common.model.page.MyPage; import com.acimage.common.redis.annotation.QueryRedis; import java.util.List; public interface CmtyUserRankService { MyPage pageUserRankBy(String column, int pageNo, int pageSize); @QueryRedis(keyPrefix = "acimage:community:users:rank:topicCount:",expire = 5L) List pageUserRankByTopicCount(int pageNo, int pageSize); @QueryRedis(keyPrefix = "acimage:community:users:rank:starCount:",expire = 5L) List pageUserRankByStarCount(int pageNo, int pageSize); } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/service/cmtyuser/CmtyUserWriteService.java ================================================ package com.acimage.community.service.cmtyuser; import com.acimage.common.model.domain.community.CmtyUser; import org.springframework.transaction.annotation.Transactional; import java.util.List; public interface CmtyUserWriteService { void updateUsername(long userId, String username); void updatePhotoUrl(long userId, String photoUrl); @Transactional void save(CmtyUser cmtyUser); Integer updateStarCountByIncrements(List userIds, List starCounts); Integer updateTopicCountByIncrements(List userIds, List starCounts); Integer updateTopicCountByIncrement(long userId, int increment); Integer updateStarCountByIncrement(long userId, int increment); } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/service/cmtyuser/impl/CmtyUserQueryServiceImpl.java ================================================ package com.acimage.community.service.cmtyuser.impl; import com.acimage.common.model.domain.community.CmtyUser; import com.acimage.common.redis.annotation.QueryRedis; import com.acimage.common.redis.enums.DataType; import com.acimage.community.dao.CmtyUserDao; import com.acimage.community.service.cmtyuser.CmtyUserQueryService; import com.acimage.community.depreted.userstatistic.consts.KeyConstants; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class CmtyUserQueryServiceImpl implements CmtyUserQueryService { @Autowired CmtyUserDao cmtyUserDao; @Override @QueryRedis(keyPrefix = KeyConstants.STRINGKP_CMTY_USER, expire = 10L, dataType = DataType.HASH) public CmtyUser getCmtyUser(long userId) { return cmtyUserDao.selectById(userId); } } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/service/cmtyuser/impl/CmtyUserRankServiceImpl.java ================================================ package com.acimage.community.service.cmtyuser.impl; import com.acimage.common.model.domain.community.CmtyUser; import com.acimage.common.model.page.MyPage; import com.acimage.common.redis.annotation.QueryRedis; import com.acimage.common.utils.LambdaUtils; import com.acimage.common.utils.common.PageUtils; import com.acimage.community.dao.CmtyUserDao; import com.acimage.community.service.cmtyuser.CmtyUserRankService; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.toolkit.StringUtils; import com.baomidou.mybatisplus.core.toolkit.support.SFunction; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; @Service public class CmtyUserRankServiceImpl implements CmtyUserRankService { @Autowired CmtyUserDao cmtyUserDao; private List pageUserRankBy(SFunction attr, int pageNo, int pageSize) { int start = PageUtils.startIndexOf(pageNo, pageSize); String column = LambdaUtils.underlineColumnNameOf(attr); List cmtyUserList = cmtyUserDao.selectListOrderByColumn(column, start, pageSize); return cmtyUserList; } @Override public MyPage pageUserRankBy(String column, int pageNo, int pageSize) { String underlineColumn = StringUtils.camelToUnderline(column); IPage page = new Page<>(pageNo, pageSize); QueryWrapper qw = new QueryWrapper<>(); qw.orderByDesc(underlineColumn); return MyPage.from(cmtyUserDao.selectPage(page, qw)); } @Override @QueryRedis(keyPrefix = "acimage:community:users:rank:topicCount:", expire = 5L) public List pageUserRankByTopicCount(int pageNo, int pageSize) { return pageUserRankBy(CmtyUser::getTopicCount, pageNo, pageSize); } @Override @QueryRedis(keyPrefix = "acimage:community:users:rank:starCount:", expire = 5L) public List pageUserRankByStarCount(int pageNo, int pageSize) { return pageUserRankBy(CmtyUser::getStarCount, pageNo, pageSize); } } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/service/cmtyuser/impl/CmtyUserWriteServiceImpl.java ================================================ package com.acimage.community.service.cmtyuser.impl; import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.lang.Pair; import com.acimage.common.model.domain.community.CmtyUser; import com.acimage.common.utils.LambdaUtils; import com.acimage.common.utils.common.PairUtils; import com.acimage.common.utils.redis.RedisUtils; import com.acimage.community.dao.CmtyUserDao; import com.acimage.community.service.cmtyuser.CmtyUserWriteService; import com.acimage.community.depreted.userstatistic.consts.KeyConstants; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; @Service public class CmtyUserWriteServiceImpl implements CmtyUserWriteService { @Autowired CmtyUserDao cmtyUserDao; @Autowired RedisUtils redisUtils; @Override public void updateUsername(long userId, String username) { LambdaUpdateWrapper uw = new LambdaUpdateWrapper<>(); uw.set(CmtyUser::getUsername, username) .eq(CmtyUser::getId, userId); cmtyUserDao.update(null, uw); redisUtils.delete(KeyConstants.STRINGKP_CMTY_USER); } @Override public void updatePhotoUrl(long userId, String photoUrl) { LambdaUpdateWrapper uw = new LambdaUpdateWrapper<>(); uw.set(CmtyUser::getPhotoUrl, photoUrl) .eq(CmtyUser::getId, userId); cmtyUserDao.update(null, uw); redisUtils.delete(KeyConstants.STRINGKP_CMTY_USER); } @Override public void save(CmtyUser cmtyUser) { cmtyUserDao.insert(cmtyUser); } @Override public Integer updateStarCountByIncrements(List userIds, List starCounts){ List> userIdAndStarCounts= PairUtils.combine(userIds,starCounts); if(CollectionUtil.isEmpty(userIds)){ return 0; } return cmtyUserDao.batchUpdateStarCount(userIdAndStarCounts); } @Override public Integer updateTopicCountByIncrements(List userIds, List starCounts){ List> userIdAndStarCounts= PairUtils.combine(userIds,starCounts); if(CollectionUtil.isEmpty(userIds)){ return 0; } return cmtyUserDao.batchUpdateTopicCount(userIdAndStarCounts); } @Override public Integer updateTopicCountByIncrement( long userId, int increment){ redisUtils.delete(KeyConstants.STRINGKP_CMTY_USER +userId); return cmtyUserDao.updateTopicCountByIncrement(userId,increment); } @Override public Integer updateStarCountByIncrement(long userId, int increment) { int col=cmtyUserDao.updateStarCountByIncrement(userId,increment); String key=KeyConstants.STRINGKP_CMTY_USER +userId; String column= LambdaUtils.columnOf(CmtyUser::getStarCount); redisUtils.incrementIfPresentForFieldKey(key,column,increment); return col; } } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/service/comment/CommentInfoQueryService.java ================================================ package com.acimage.community.service.comment; import com.acimage.common.model.domain.community.Comment; import com.acimage.common.model.page.MyPage; import java.util.List; public interface CommentInfoQueryService { List pageCommentsWithUser(long topicId, int pageNo,int pageSize); MyPage pageCommentsWithTopicOrderByCreateTime(long userId, int pageNo,int pageSize); } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/service/comment/CommentQueryService.java ================================================ package com.acimage.community.service.comment; import com.acimage.common.model.domain.community.Comment; public interface CommentQueryService { Comment getComment(long commentId); Integer getCommentCount(long topicId); } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/service/comment/CommentWriteService.java ================================================ package com.acimage.community.service.comment; import com.acimage.community.model.request.CommentAddReq; import com.acimage.community.model.request.CommentModifyReq; import org.springframework.transaction.annotation.Transactional; public interface CommentWriteService { Integer saveComment(CommentAddReq commentAddReq); Integer removeComment(long commentId); Integer removeCommentWithoutVerification(long commentId); Integer removeComments(long topicId); Integer updateComment(CommentModifyReq commentModifyReq); } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/service/comment/annotation/Operation.java ================================================ package com.acimage.community.service.comment.annotation; public enum Operation { ADD, SUB, REMOVE_ALL } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/service/comment/annotation/UpdateCcByReturn.java ================================================ package com.acimage.community.service.comment.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 根据被注解的方法的返回值更改对应评论的话题数 */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface UpdateCcByReturn { Operation operation() default Operation.ADD; } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/service/comment/impl/CommentInfoQueryServiceImpl.java ================================================ package com.acimage.community.service.comment.impl; import com.acimage.common.redis.annotation.KeyParam; import com.acimage.common.redis.annotation.QueryRedis; import com.acimage.common.model.domain.community.Comment; import com.acimage.common.utils.common.PageUtils; import com.acimage.community.dao.CommentDao; import com.acimage.common.model.page.MyPage; import com.acimage.community.service.comment.CommentInfoQueryService; import com.acimage.community.global.consts.CommentKeyConstants; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; import java.util.concurrent.TimeUnit; @Service public class CommentInfoQueryServiceImpl implements CommentInfoQueryService { @Autowired CommentDao commentDao; @QueryRedis(keyPrefix = CommentKeyConstants.STRINGKP_TOPIC_COMMENTS, expire = 3L, unit = TimeUnit.SECONDS) public List pageCommentsWithUser(long topicId, int pageNo, int pageSize) { //如果没有则查数据库 int startIndex = PageUtils.startIndexOf(pageNo, pageSize); return commentDao.selectCommentsWithUser(topicId, startIndex, pageSize); } @QueryRedis(keyPrefix = CommentKeyConstants.STRINGKP_USER_COMMENTS, expire = 5, unit = TimeUnit.SECONDS) @Override public MyPage pageCommentsWithTopicOrderByCreateTime(long userId, int pageNo, int pageSize) { int startIndex = PageUtils.startIndexOf(pageNo, pageSize); List comments = commentDao.selectCommentsWithTopicOrderByCreateTime(userId, startIndex, pageSize); //查询总数 LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); qw.eq(Comment::getUserId, userId); int totalCount = commentDao.selectCount(qw); return new MyPage<>(totalCount, comments); } } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/service/comment/impl/CommentQueryServiceImpl.java ================================================ package com.acimage.community.service.comment.impl; import com.acimage.common.redis.annotation.KeyParam; import com.acimage.common.redis.annotation.QueryRedis; import com.acimage.common.model.domain.community.Comment; import com.acimage.community.dao.CommentDao; import com.acimage.community.service.comment.CommentQueryService; import com.acimage.community.global.consts.CommentKeyConstants; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.concurrent.TimeUnit; @Service public class CommentQueryServiceImpl implements CommentQueryService { @Autowired CommentDao commentDao; @QueryRedis(keyPrefix = "acimage:comments:id:",expire = 5L, unit = TimeUnit.SECONDS) @Override public Comment getComment(@KeyParam long commentId) { return commentDao.selectById(commentId); } @QueryRedis(keyPrefix = CommentKeyConstants.STRINGKP_COMMENT_COUNT,expire = 129L) @Override public Integer getCommentCount(@KeyParam long topicId) { LambdaQueryWrapper qw=new LambdaQueryWrapper<>(); qw.eq(Comment::getTopicId,topicId); return commentDao.selectCount(qw); } } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/service/comment/impl/CommentWriteServiceImpl.java ================================================ package com.acimage.community.service.comment.impl; import cn.hutool.core.bean.BeanUtil; import com.acimage.common.model.domain.community.Topic; import com.acimage.common.model.domain.user.CommentMsg; import com.acimage.common.utils.SensitiveWordUtils; import com.acimage.common.utils.common.BeanUtils; import com.acimage.common.utils.common.ListUtils; import com.acimage.common.global.context.UserContext; import com.acimage.common.global.exception.BusinessException; import com.acimage.common.model.domain.community.Comment; import com.acimage.community.listener.event.CommentEvent; import com.acimage.community.model.request.CommentAddReq; import com.acimage.community.model.request.CommentModifyReq; import com.acimage.community.mq.producer.UserMsgMqProducer; import com.acimage.community.service.comment.CommentQueryService; import com.acimage.community.service.comment.CommentWriteService; import com.acimage.community.global.consts.CommentKeyConstants; import com.acimage.common.utils.IdGenerator; import com.acimage.common.utils.redis.RedisUtils; import com.acimage.community.dao.CommentDao; import com.acimage.community.service.topic.TopicQueryService; import com.acimage.community.service.topic.TopicSpAttrWriteService; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.Date; import java.util.List; import java.util.concurrent.TimeUnit; @Service @Slf4j public class CommentWriteServiceImpl implements CommentWriteService { @Autowired CommentDao commentDao; @Autowired CommentQueryService commentQueryService; @Autowired RedisUtils redisUtils; @Autowired TopicSpAttrWriteService topicSpAttrWriteService; @Autowired TopicQueryService topicQueryService; @Autowired UserMsgMqProducer userMsgMqProducer; @Override public Integer saveComment(CommentAddReq commentAddReq) { String publishedContent = redisUtils.getForString(CommentKeyConstants.STRINGKP_PUBLISHED_COMMENTS); if (publishedContent != null && publishedContent.equals(commentAddReq.getContent())) { log.warn("user:{}重复发表评论 title:{}", UserContext.getUsername(), commentAddReq.getContent()); throw new BusinessException("短期已经发表过该评论了,请刷新尝试"); } Comment comment = new Comment(); BeanUtil.copyProperties(commentAddReq, comment); Date now = new Date(); long id = IdGenerator.getSnowflakeNextId(); comment.setId(id); comment.setUserId(UserContext.getUserId()); comment.setCreateTime(now); comment.setUpdateTime(now); //敏感词过滤 String filterContent = SensitiveWordUtils.filter(commentAddReq.getContent()); comment.setContent(filterContent); int col = commentDao.insert(comment); long topicId = commentAddReq.getTopicId(); //删除首页评论 String firstPageKey = CommentKeyConstants.keyOfTopicComments(topicId, 1); redisUtils.delete(firstPageKey); //更新评论数 topicSpAttrWriteService.increaseCommentCount(topicId, col); //更新话题的最新活跃时间 topicSpAttrWriteService.changeActivityTime(topicId, new Date()); //发送消息通知用户 Topic topic = topicQueryService.getTopic(topicId); CommentMsg commentMsg = CommentMsg.builder() .commentId(id) .content(filterContent) .createTime(now) .fromUserId(UserContext.getUserId()) .toUserId(topic.getUserId()) .topicTitle(topic.getTitle()) .topicId(topicId) .build(); userMsgMqProducer.sendCommentMessage(commentMsg); long timeout = 10L; redisUtils.setAsString(CommentKeyConstants.STRINGKP_PUBLISHED_COMMENTS + UserContext.getUserId(), commentAddReq.getContent(), timeout, TimeUnit.SECONDS); return col; } @Override public Integer removeComment(long commentId) { Comment comment = commentQueryService.getComment(commentId); if (comment == null) { log.error("user:{} 删除评论{} 错误:评论不存在", UserContext.getUsername(), commentId); throw new BusinessException("评论不存在"); } if (!comment.getUserId().equals(UserContext.getUserId())) { log.error("user:{} 删除 评论{} 错误:非评论持有者", UserContext.getUsername(), commentId); throw new BusinessException("非法操作"); } int col = commentDao.deleteById(commentId); //如果影响redis话题首页评论,则删除 long topicId = comment.getTopicId(); String firstPageKey = CommentKeyConstants.keyOfTopicComments(topicId, 1); redisUtils.delete(firstPageKey); //更新评论数 topicSpAttrWriteService.increaseCommentCount(topicId, -col); return col; } @Override public Integer removeCommentWithoutVerification(long commentId) { Comment comment = commentQueryService.getComment(commentId); if (comment == null) { log.error("user:{} 删除评论{} 错误:评论不存在", UserContext.getUsername(), commentId); throw new BusinessException("评论不存在"); } int col = commentDao.deleteById(commentId); //如果影响redis话题首页评论,则删除 long topicId = comment.getTopicId(); String firstPageKey = CommentKeyConstants.keyOfTopicComments(topicId, 1); redisUtils.delete(firstPageKey); //更新评论数 topicSpAttrWriteService.increaseCommentCount(topicId, -col); return col; } /** * 删除话题时调用 */ @Override public Integer removeComments(long topicId) { int col = commentDao.deleteByTopicId(topicId); //删除redis数据 redisUtils.delete(CommentKeyConstants.STRINGKP_TOPIC_COMMENTS + topicId); return col; } @Override public Integer updateComment(CommentModifyReq commentModifyReq) { Date now = new Date(); Comment modifiedComment = BeanUtils.copyPropertiesTo(commentModifyReq, Comment.class); //过滤敏感词 String filterContent = SensitiveWordUtils.filter(commentModifyReq.getContent()); modifiedComment.setContent(filterContent); modifiedComment.setUpdateTime(now); log.info("评论修改为{}", modifiedComment); long commentId = commentModifyReq.getId(); LambdaUpdateWrapper uw = new LambdaUpdateWrapper<>(); uw.eq(Comment::getId, commentId) .eq(Comment::getUserId, UserContext.getUserId()); int col = commentDao.update(modifiedComment, uw); if (col == 0) { log.error("用户{} 修改 评论{} 错误:评论已被删除或评论非当前用户所有", UserContext.getUsername(), commentId); throw new BusinessException("非法操作,更新失败"); } //找到评论 Comment originComment = commentQueryService.getComment(commentId); //如果影响redis话题首页评论,则删除redis数据 long topicId = originComment.getTopicId(); String firstPageCommentsKey = CommentKeyConstants.keyOfTopicComments(topicId, 1); List comments = redisUtils.getListFromString(firstPageCommentsKey, Comment.class); if (comments != null && ListUtils.extract(Comment::getId, comments).contains(commentId)) { redisUtils.delete(firstPageCommentsKey); } //更新对应话题的最新活跃时间 topicSpAttrWriteService.changeActivityTime(topicId, now); return col; } } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/service/homecarousel/HomeCarouselQueryService.java ================================================ package com.acimage.community.service.homecarousel; import com.acimage.common.model.domain.community.HomeCarousel; import java.util.List; public interface HomeCarouselQueryService { List listAll(); } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/service/homecarousel/impl/HomeCarouselQueryServiceImpl.java ================================================ package com.acimage.community.service.homecarousel.impl; import com.acimage.common.model.domain.community.HomeCarousel; import com.acimage.common.redis.annotation.QueryRedis; import com.acimage.community.service.homecarousel.HomeCarouselQueryService; import com.acimage.community.dao.HomeCarrouselDao; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; @Service public class HomeCarouselQueryServiceImpl implements HomeCarouselQueryService { public static final String STRINGK_HOME_CAROUSEL = "acimage:community:homeCarousels:list"; @Autowired HomeCarrouselDao homeCarrouselDao; @QueryRedis(keyPrefix = STRINGK_HOME_CAROUSEL, expire = 2L ) @Override public List listAll() { LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); qw.orderByAsc(HomeCarousel::getLocation); return homeCarrouselDao.selectList(qw); } } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/service/star/StarMixQueryService.java ================================================ package com.acimage.community.service.star; import com.acimage.common.model.domain.community.Star; import com.acimage.common.model.page.MyPage; public interface StarMixQueryService { MyPage pageStarsWithTopic(long userId, int pageNo); } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/service/star/StarQueryService.java ================================================ package com.acimage.community.service.star; public interface StarQueryService { boolean isStar(long userId,long topicId); Integer getTopicStarCount(long topicId); Integer getStarCountOwnedBy(long userId); } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/service/star/StarWriteService.java ================================================ package com.acimage.community.service.star; public interface StarWriteService { void saveStar(long userId,long topicId); void removeStar(long userId,long topicId); void removeStars(long topicId); } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/service/star/impl/StarMixQueryServiceImpl.java ================================================ package com.acimage.community.service.star.impl; import com.acimage.common.model.domain.community.Star; import com.acimage.common.redis.annotation.QueryRedis; import com.acimage.community.dao.StarDao; import com.acimage.community.global.consts.PageSizeConstants; import com.acimage.common.model.page.MyPage; import com.acimage.community.service.star.StarMixQueryService; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; import java.util.concurrent.TimeUnit; @Service public class StarMixQueryServiceImpl implements StarMixQueryService { @Autowired StarDao starDao; @QueryRedis(keyPrefix = "acimage:stars:userId:pageNo:", expire = 30L, unit = TimeUnit.SECONDS) @Override public MyPage pageStarsWithTopic(long userId, int pageNo) { int startIndex = (pageNo - 1) * PageSizeConstants.ACTIVITY_STARS; List stars = starDao.selectStarsWithTopicOrderByCreateTime(userId, startIndex, PageSizeConstants.ACTIVITY_STARS); LambdaUpdateWrapper qw=new LambdaUpdateWrapper<>(); qw.eq(Star::getUserId,userId); int totalCount = starDao.selectCount(qw); return new MyPage<>(totalCount, stars); } } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/service/star/impl/StarQueryServiceImpl.java ================================================ package com.acimage.community.service.star.impl; import com.acimage.common.model.domain.community.Star; import com.acimage.common.redis.annotation.KeyParam; import com.acimage.common.redis.annotation.QueryRedis; import com.acimage.community.dao.StarDao; import com.acimage.community.service.star.StarQueryService; import com.acimage.community.global.consts.StarKeyConstants; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class StarQueryServiceImpl implements StarQueryService { @Autowired StarDao starDao; @QueryRedis(keyPrefix = StarKeyConstants.STRINGKP_STAR_USER_TOPIC) @Override public boolean isStar(@KeyParam long userId,@KeyParam long topicId) { LambdaQueryWrapper qw=new LambdaQueryWrapper<>(); qw.eq(Star::getUserId,userId) .eq(Star::getTopicId,topicId); return starDao.selectOne(qw) != null; } @QueryRedis(keyPrefix = StarKeyConstants.STRINGKP_TOPIC_STAR_COUNT,expire = 31L) @Override public Integer getTopicStarCount(@KeyParam long topicId) { LambdaQueryWrapper qw=new LambdaQueryWrapper<>(); qw.eq(Star::getTopicId,topicId); return starDao.selectCount(qw); } @QueryRedis(keyPrefix = StarKeyConstants.STRINGKP_USER_STAR_COUNT, expire = 31L) @Override public Integer getStarCountOwnedBy(@KeyParam long userId) { return starDao.countStarsOwnedBy(userId); } } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/service/star/impl/StarWriteServiceImpl.java ================================================ package com.acimage.community.service.star.impl; import com.acimage.common.global.context.UserContext; import com.acimage.common.model.domain.community.Topic; import com.acimage.common.global.exception.BusinessException; import com.acimage.common.model.domain.community.Star; import com.acimage.community.service.cmtyuser.CmtyUserWriteService; import com.acimage.community.global.consts.StarKeyConstants; import com.acimage.community.service.star.StarQueryService; import com.acimage.community.service.topic.TopicQueryService; import com.acimage.community.service.star.StarWriteService; import com.acimage.common.utils.redis.RedisUtils; import com.acimage.community.dao.StarDao; import com.acimage.community.service.topic.TopicSpAttrWriteService; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DuplicateKeyException; import org.springframework.stereotype.Service; import java.util.Date; import java.util.concurrent.TimeUnit; @Slf4j @Service public class StarWriteServiceImpl implements StarWriteService { @Autowired StarDao starDao; @Autowired StarQueryService starQueryService; @Autowired RedisUtils redisUtils; @Autowired TopicQueryService topicQueryService; @Autowired TopicSpAttrWriteService topicSpAttrWriteService; @Autowired CmtyUserWriteService cmtyUserWriteService; @Override public void saveStar(long userId, long topicId) { if (starQueryService.isStar(userId, topicId)) { log.info("user:{}已经点赞过话题topicId:{}", UserContext.getUsername(), topicId); throw new BusinessException("已经点过赞了"); } Star star = new Star(userId, topicId); star.setUserId(userId); star.setTopicId(topicId); star.setCreateTime(new Date()); try { starDao.insert(star); } catch (DuplicateKeyException e) { log.warn("user:{} 收藏过了话题 topicId:{}", UserContext.getUsername(), topicId); throw new BusinessException("已经收藏过了,请刷新重试"); } int increment = 1; topicSpAttrWriteService.increaseStarCount(topicId, increment); //更新主人star Topic topic = topicQueryService.getTopic(topicId); if (topic != null) { cmtyUserWriteService.updateStarCountByIncrement(topic.getUserId(), increment); } long timeout = 3L; redisUtils.setObjectJson(StarKeyConstants.keyOfIsStar(userId, topicId), Boolean.TRUE, timeout, TimeUnit.SECONDS); } @Override public void removeStar(long userId, long topicId) { if (!starQueryService.isStar(userId, topicId)) { log.info("user:{}已经取消点赞过话题topicId:{}", UserContext.getUsername(), topicId); throw new BusinessException("已经取消过点赞了"); } int col = starDao.deleteByUserIdAndTopicId(userId, topicId); //删除缓存 redisUtils.delete(StarKeyConstants.keyOfIsStar(userId, topicId)); //更新话题、话题主人收藏量 int starIncrement = -col; topicSpAttrWriteService.increaseStarCount(topicId, starIncrement); Topic topic = topicQueryService.getTopic(topicId); if (topic != null) { long ownerId = topic.getUserId(); cmtyUserWriteService.updateStarCountByIncrement(ownerId, starIncrement); } } @Override public void removeStars(long topicId) { LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); qw.eq(Star::getTopicId, topicId); int starIncrement = -starDao.delete(qw); //更新话题主人收藏量 Topic topic = topicQueryService.getTopic(topicId); if (topic != null) { long ownerId = topic.getUserId(); cmtyUserWriteService.updateStarCountByIncrement(ownerId, starIncrement); } } } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/service/tag/TagQueryService.java ================================================ package com.acimage.community.service.tag; import com.acimage.common.model.domain.community.Tag; import java.util.List; public interface TagQueryService { List listAll(); List checkAndListTags(List tagIds); } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/service/tag/TagTopicQueryService.java ================================================ package com.acimage.community.service.tag; import java.util.List; public interface TagTopicQueryService { List listTagIds(long topicId); } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/service/tag/TagTopicWriteService.java ================================================ package com.acimage.community.service.tag; import java.util.List; public interface TagTopicWriteService { void save(long topicId, List tagIdList); void remove(long topicId); } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/service/tag/TagWriteService.java ================================================ package com.acimage.community.service.tag; import java.util.List; public interface TagWriteService { } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/service/tag/impl/TagQueryServiceImpl.java ================================================ package com.acimage.community.service.tag.impl; import cn.hutool.core.collection.CollectionUtil; import com.acimage.common.global.exception.BusinessException; import com.acimage.common.global.context.UserContext; import com.acimage.common.model.domain.community.Tag; import com.acimage.common.utils.common.ListUtils; import com.acimage.community.dao.TagDao; import com.acimage.community.service.tag.TagQueryService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.List; @Slf4j @Service public class TagQueryServiceImpl implements TagQueryService { @Autowired TagDao tagDao; @Override public List listAll() { return tagDao.selectList(null); } @Override public List checkAndListTags(List tagIds) { if (CollectionUtil.isEmpty(tagIds)) { return new ArrayList<>(); } List allTags = listAll(); List allTagIds = ListUtils.extract(Tag::getId, allTags); for (Integer tagId : tagIds) { if (!allTagIds.contains(tagId)) { log.error("cmtyUser:{} error:标签不存在 tagId:{}", UserContext.getUsername(), tagId); throw new BusinessException("标签不存在"); } } int i = 0; List tags = new ArrayList<>(); while (i < tagIds.size()-1) { for (Tag item : allTags) { if (item.getId().equals(tagIds.get(i))) { tags.add(item); i++; break; } } } return tags; } } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/service/tag/impl/TagTopicQueryServiceImpl.java ================================================ package com.acimage.community.service.tag.impl; import com.acimage.common.redis.annotation.QueryRedis; import com.acimage.community.dao.TagTopicDao; import com.acimage.community.service.tag.TagTopicQueryService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; @Service public class TagTopicQueryServiceImpl implements TagTopicQueryService { @Autowired TagTopicDao tagTopicDao; @QueryRedis(keyPrefix = "acimage:community:tags:topicId:") @Override public List listTagIds(long topicId){ return tagTopicDao.selectTagIds(topicId); } } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/service/tag/impl/TagTopicWriteServiceImpl.java ================================================ package com.acimage.community.service.tag.impl; import cn.hutool.core.collection.CollectionUtil; import com.acimage.common.model.domain.community.TagTopic; import com.acimage.common.utils.IdGenerator; import com.acimage.community.dao.TagTopicDao; import com.acimage.community.service.tag.TagQueryService; import com.acimage.community.service.tag.TagTopicWriteService; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.Date; import java.util.List; @Slf4j @Service public class TagTopicWriteServiceImpl implements TagTopicWriteService { @Autowired TagTopicDao tagTopicDao; @Autowired TagQueryService tagQueryService; @Override public void save(long topicId, List tagIdList) { // //检查标签是否存在 // List tagList = tagQueryService.listAll(); // List allTagIds = ListUtils.extract(Tag::getId, tagList); // for (Integer tagId : tagIdList) { // if (!allTagIds.contains(tagId)) { // log.error("cmtyUser:{} error:标签不存在 tagId:{}", UserContext.getUsername(), tagId); // throw new BusinessException("标签不存在"); // } // } if (CollectionUtil.isEmpty(tagIdList)) { return; } List tagTopicList = new ArrayList<>(); Date now = new Date(); for (Integer tagId : tagIdList) { TagTopic tagTopic = TagTopic.builder() .id(IdGenerator.getSnowflakeNextId()) .topicId(topicId) .tagId(tagId) .createTime(now) .build(); tagTopicList.add(tagTopic); } tagTopicDao.insertBatch(tagTopicList); } @Override public void remove(long topicId){ LambdaQueryWrapper qw=new LambdaQueryWrapper<>(); qw.eq(TagTopic::getTopicId,topicId); tagTopicDao.delete(qw); } } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/service/tag/impl/TagWriteServiceImpl.java ================================================ package com.acimage.community.service.tag.impl; import com.acimage.community.service.tag.TagWriteService; import org.springframework.stereotype.Service; @Service public class TagWriteServiceImpl implements TagWriteService { } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/service/topic/Impl/TopicEsSearchServiceImpl.java ================================================ package com.acimage.community.service.topic.Impl; import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.util.StrUtil; import com.acimage.common.global.exception.BusinessException; import com.acimage.common.model.Index.TopicIndex; import com.acimage.common.model.domain.community.Category; import com.acimage.common.model.domain.community.Topic; import com.acimage.common.model.page.MyPage; import com.acimage.common.utils.EsUtils; import com.acimage.common.utils.LambdaUtils; import com.acimage.common.utils.common.ListUtils; import com.acimage.community.global.enums.SortMode; import com.acimage.community.model.request.TopicQueryByCategoryIdReq; import com.acimage.community.model.request.TopicQueryBySortReq; import com.acimage.community.model.request.TopicQueryByTagIdReq; import com.acimage.community.model.request.TopicSearchReq; import com.acimage.community.service.categoty.CategoryQueryService; import com.acimage.community.service.tag.TagQueryService; import com.acimage.community.service.topic.TopicEsSearchService; import lombok.extern.slf4j.Slf4j; import org.elasticsearch.index.query.BoolQueryBuilder; import org.elasticsearch.index.query.MultiMatchQueryBuilder; import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder; import org.elasticsearch.search.sort.FieldSortBuilder; import org.elasticsearch.search.sort.SortOrder; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.PageRequest; import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate; import org.springframework.data.elasticsearch.core.SearchHit; import org.springframework.data.elasticsearch.core.SearchHits; import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder; import org.springframework.stereotype.Service; import java.util.*; import java.util.stream.Collectors; @Slf4j @Service public class TopicEsSearchServiceImpl implements TopicEsSearchService { @Autowired EsUtils esUtils; @Autowired ElasticsearchRestTemplate esTemplate; @Autowired CategoryQueryService categoryQueryService; @Autowired TagQueryService tagQueryService; @Override public List searchSimilar(long topicId, int size) { List columns = LambdaUtils.columnsFrom(Topic::getContent, Topic::getTitle); List topicIndices = esUtils.similarQuery(Long.toString(topicId), TopicIndex.class, columns, 1, size); return TopicIndex.toTopicList(topicIndices); } @Override public List searchSimilarByTitle(long topicId, String title, int size) { String column = LambdaUtils.columnOf(TopicIndex::getTitle); float score = 0.2f; List topicIndices = esUtils.matchQuery(TopicIndex.class, column, title, 1, size + 1, score); List topicList = TopicIndex.toTopicList(topicIndices).stream() .filter(o -> !o.getId().equals(topicId)) .collect(Collectors.toList()); if (topicList.size() == size + 1) { return topicList.subList(0, size); } else { return topicList; } } @Override public MyPage searchByTagId(TopicQueryByTagIdReq queryReq) { Integer tagId=queryReq.getTagId(); int pageNo=queryReq.getPageNo(); int pageSize=queryReq.getPageSize(); SortMode sortMode=queryReq.getSortMode(); //检查tagId是否存在 tagQueryService.checkAndListTags(Collections.singletonList(tagId)); //获取sortBuilder FieldSortBuilder sortBuilder=SortMode.toSortBuilder(sortMode); String column = LambdaUtils.columnOf(Topic::getTagIds); MyPage topicIndexPage = esUtils.termQuery(column, tagId, TopicIndex.class, pageNo, pageSize,sortBuilder); return TopicIndex.toTopicPage(topicIndexPage); } @Override public MyPage searchBySort(TopicQueryByCategoryIdReq queryReq) { int categoryId=queryReq.getCategoryId(); int pageNo=queryReq.getPageNo(); int pageSize=queryReq.getPageSize(); SortMode sortMode=queryReq.getSortMode(); FieldSortBuilder sortBuilder=SortMode.toSortBuilder(sortMode); //校验分类是否存在 List categoryIds = ListUtils.extract(Category::getId, categoryQueryService.listAll()); if(!categoryIds.contains(categoryId)){ log.warn("用户查询分类 id:{}不存在",categoryId); throw new BusinessException("分类不存在"); } String column=LambdaUtils.columnOf(TopicIndex::getCategoryId); MyPage topicIndexPage=esUtils.termQuery(column,categoryId, TopicIndex.class,pageNo,pageSize,sortBuilder); return TopicIndex.toTopicPage(topicIndexPage); } @Override public MyPage searchBySort(TopicQueryBySortReq queryReq) { int pageNo=queryReq.getPageNo(); int pageSize=queryReq.getPageSize(); SortMode sortMode=queryReq.getSortMode(); FieldSortBuilder sortBuilder=SortMode.toSortBuilder(sortMode); MyPage topicIndexPage=esUtils.queryBySort(TopicIndex.class,pageNo,pageSize,sortBuilder); return TopicIndex.toTopicPage(topicIndexPage); } @Override public MyPage search(TopicSearchReq topicSearchReq) { String search = topicSearchReq.getSearch(); Integer categoryId = topicSearchReq.getCategoryId(); Integer tagId = topicSearchReq.getTagId(); Integer pageNo = topicSearchReq.getPageNo(); SortMode sort = topicSearchReq.getSortMode(); BoolQueryBuilder boolQuery = QueryBuilders.boolQuery(); HighlightBuilder highlightBuilder = null; String titleColumn = LambdaUtils.columnOf(TopicIndex::getTitle); String contentColumn = LambdaUtils.columnOf(TopicIndex::getContent); if (!StrUtil.isBlank(search)) { //有关键词则高亮 HighlightBuilder.Field titleField = new HighlightBuilder.Field(titleColumn); HighlightBuilder.Field contentField = new HighlightBuilder.Field(contentColumn); MultiMatchQueryBuilder matchQuery = QueryBuilders.multiMatchQuery(search, titleColumn, contentColumn); highlightBuilder = new HighlightBuilder() .field(titleField) .field(contentField) .preTags("") .postTags(""); boolQuery.must().add(matchQuery); } if (tagId != null) { String tagIdsColumn = LambdaUtils.columnOf(TopicIndex::getTagIds); boolQuery.filter().add(QueryBuilders.termQuery(tagIdsColumn, tagId)); } if (categoryId != null) { String categoryIdColumn = LambdaUtils.columnOf(TopicIndex::getCategoryId); boolQuery.filter().add(QueryBuilders.termQuery(categoryIdColumn, categoryId)); } NativeSearchQueryBuilder nativeSearchQuery = new NativeSearchQueryBuilder() .withQuery(boolQuery) .withPageable(PageRequest.of(pageNo - 1, 10)); if (highlightBuilder != null) { nativeSearchQuery.withHighlightBuilder(highlightBuilder); } if (sort != null && sort.toColumn() != null) { FieldSortBuilder sortBuilder = new FieldSortBuilder(Objects.requireNonNull(sort.toColumn())) .order(SortOrder.DESC); nativeSearchQuery.withSort(sortBuilder); } //整合结果 List topicIndexList = new ArrayList<>(); SearchHits searchHits = esTemplate.search(nativeSearchQuery.build(), TopicIndex.class); int total = (int) searchHits.getTotalHits(); for (SearchHit searchHit : searchHits.getSearchHits()) { TopicIndex topicIndex = searchHit.getContent(); if (!StrUtil.isBlank(search)) { //title高亮 StringBuilder newTitle = new StringBuilder(); List titleHighlights = searchHit.getHighlightFields().get(titleColumn); if (!CollectionUtil.isEmpty(titleHighlights)) { if (titleHighlights.size() > 0) { for (String highlight : titleHighlights) { newTitle.append(highlight); } } topicIndex.setTitle(newTitle.toString()); } //content高亮 StringBuilder newContent = new StringBuilder(); List contentHighlights = searchHit.getHighlightFields().get(contentColumn); if (!CollectionUtil.isEmpty(contentHighlights)) { if (contentHighlights.size() > 0) { for (String highlight : contentHighlights) { newContent.append(highlight) .append(" "); } } topicIndex.setContent(newContent.toString()); } } topicIndexList.add(topicIndex); } return new MyPage<>(total, TopicIndex.toTopicList(topicIndexList)); } } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/service/topic/Impl/TopicHtmlQueryServiceImpl.java ================================================ package com.acimage.community.service.topic.Impl; import com.acimage.common.model.domain.community.TopicHtml; import com.acimage.common.redis.annotation.QueryRedis; import com.acimage.common.utils.SensitiveWordUtils; import com.acimage.community.dao.TopicHtmlDao; import com.acimage.community.service.topic.TopicHtmlQueryService; import com.acimage.community.global.consts.TopicKeyConstants; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class TopicHtmlQueryServiceImpl implements TopicHtmlQueryService { @Autowired TopicHtmlDao topicHtmlDao; @Override @QueryRedis(keyPrefix = TopicKeyConstants.HASHKP_TOPIC_HTML, expire = 12L) public TopicHtml getTopicHtml(long topicId) { TopicHtml topicHtml = topicHtmlDao.selectById(topicId); topicHtml.setHtml(SensitiveWordUtils.filter(topicHtml.getHtml())); return topicHtml; } } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/service/topic/Impl/TopicHtmlWriteServiceImpl.java ================================================ package com.acimage.community.service.topic.Impl; import com.acimage.common.model.domain.community.TopicHtml; import com.acimage.common.utils.redis.RedisUtils; import com.acimage.community.dao.TopicHtmlDao; import com.acimage.community.service.topic.TopicHtmlWriteService; import com.acimage.community.global.consts.TopicKeyConstants; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class TopicHtmlWriteServiceImpl implements TopicHtmlWriteService { @Autowired TopicHtmlDao topicHtmlDao; @Autowired RedisUtils redisUtils; @Override public TopicHtml save(long topicId, String html) { TopicHtml topicHtml = new TopicHtml(); topicHtml.setTopicId(topicId); topicHtml.setHtml(html); topicHtmlDao.insert(topicHtml); return topicHtml; } @Override public void remove(long topicId) { topicHtmlDao.deleteById(topicId); redisUtils.delete(TopicKeyConstants.HASHKP_TOPIC_HTML+topicId); } @Override public void update(long topicId, String html) { LambdaUpdateWrapper uw=new LambdaUpdateWrapper<>(); uw.eq(TopicHtml::getTopicId,topicId) .set(TopicHtml::getHtml,html); topicHtmlDao.update(null,uw); redisUtils.delete(TopicKeyConstants.HASHKP_TOPIC_HTML+topicId); } } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/service/topic/Impl/TopicInfoQueryServiceImpl.java ================================================ package com.acimage.community.service.topic.Impl; import cn.hutool.core.bean.BeanUtil; import com.acimage.common.global.exception.BusinessException; import com.acimage.common.global.context.UserContext; import com.acimage.common.model.domain.community.CmtyUser; import com.acimage.common.model.domain.community.Comment; import com.acimage.common.model.domain.community.Topic; import com.acimage.common.model.domain.user.User; import com.acimage.common.redis.annotation.QueryRedis; import com.acimage.common.utils.LambdaUtils; import com.acimage.common.utils.common.BeanUtils; import com.acimage.common.utils.common.PageUtils; import com.acimage.community.dao.TopicDao; import com.acimage.common.model.page.MyPage; import com.acimage.community.global.consts.PageSizeConstants; import com.acimage.community.model.vo.TopicInfoVo; import com.acimage.community.service.cmtyuser.CmtyUserQueryService; import com.acimage.community.service.comment.CommentInfoQueryService; import com.acimage.community.service.tag.TagTopicQueryService; import com.acimage.community.service.topic.*; import com.acimage.community.global.enums.TopicAttribute; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; @Slf4j @Service public class TopicInfoQueryServiceImpl implements TopicInfoQueryService { @Autowired TopicDao topicDao; @Autowired CommentInfoQueryService commentInfoQueryService; @Autowired TopicQueryService topicQueryService; @Autowired TopicSpAttrQueryService topicSpQueryService; @Autowired TopicRankQueryService topicRankQueryService; @Autowired TopicEsSearchService topicEsSearchService; @Autowired CmtyUserQueryService cmtyUserQueryService; @Autowired TagTopicQueryService tagTopicQueryService; @Autowired TopicHtmlQueryService topicHtmlQueryService; @Override public Topic getTopicWithUserTagIds(long topicId) { Topic topic = topicQueryService.getTopic(topicId); if (topic == null) { return null; } List tagIds = tagTopicQueryService.listTagIds(topicId); topic.setTagIds(tagIds); return topic; } @Override public TopicInfoVo getTopicInfoAndFirstCommentPage(long topicId) { Topic topic = this.getTopicWithUserTagIds(topicId); if (topic == null) { log.warn("user:{} 查询 话题:{} error:不存在或已被删除", UserContext.getUsername(), topicId); throw new BusinessException("话题不存在或已被删除"); } TopicInfoVo topicInfoVo = new TopicInfoVo(); //设置浏览量、收藏量、评论数 topicSpQueryService.setAttrIntoTopic(topic, TopicAttribute.COMMENT_COUNT, TopicAttribute.STAR_COUNT, TopicAttribute.PAGE_VIEW); BeanUtil.copyProperties(topic, topicInfoVo, false); //查找首页评论 int pageNo = 1; List comments = commentInfoQueryService.pageCommentsWithUser(topicId, pageNo, PageSizeConstants.TOPIC_COMMENTS); topicInfoVo.setComments(comments); CmtyUser cmtyUser = cmtyUserQueryService.getCmtyUser(topic.getUserId()); User user = BeanUtils.copyPropertiesTo(cmtyUser, User.class); topicInfoVo.setUser(user); String html = topicHtmlQueryService.getTopicHtml(topicId).getHtml(); topicInfoVo.setHtml(html); topicInfoVo.setSimilarTopics(topicEsSearchService.searchSimilarByTitle(topicId, topicInfoVo.getTitle(), 10)); return topicInfoVo; } @QueryRedis(keyPrefix = "acimage:community:topics:userId:",expire = 8L,unit = TimeUnit.SECONDS) @Override public MyPage pageUserTopicsInfoOrderByCreateTime(long userId, int pageNo, int pageSize) { int starIndex = PageUtils.startIndexOf(pageNo, pageSize); List topics = topicDao.selectTopicsWithUserOrderByCreateTime(userId, starIndex, pageSize); return new MyPage<>(topicDao.countTopics(userId), topics); } @Override public List listTopicsInfoSortBy(TopicAttribute attr, int pageNo, int pageSize) { List rankList = topicRankQueryService.listTopicIdsInRank(attr, pageNo, pageSize); List topicList = new ArrayList<>(); if (pageNo == 1 && rankList.size() < pageSize) { String column = attr.toUnderlineColumnName(); topicList = topicDao.selectTopicsWithUserOrderBy(column, pageSize); } else { for (Long topicId : rankList) { Topic topic = getTopicWithUserTagIds(topicId); if (topic != null) { topicList.add(topic); } } } for (Topic topic : topicList) { //设置浏览量 topicSpQueryService.setAttrIntoTopic(topic, TopicAttribute.PAGE_VIEW); } return topicList; } @Override public List listRandomTopicsInRank(int size) { //获取随机属性 TopicAttribute[] attrs = TopicAttribute.values(); int len = attrs.length; int i = (int) (System.currentTimeMillis() % len); TopicAttribute attr=attrs[i]; //从随机属性排行中获取随机话题 List rankList = topicRankQueryService.listRandomTopicIdsInRank(attr, size); List topicList = new ArrayList<>(); for (Long topicId : rankList) { Topic topic = getTopicWithUserTagIds(topicId); if (topic != null) { topicList.add(topic); } } for (Topic topic : topicList) { //设置浏览量 topicSpQueryService.setAttrIntoTopic(topic, TopicAttribute.PAGE_VIEW); } return topicList; } @Override public MyPage pageTopicsInfoRankByActivityTime(int pageNo, int pageSize) { List topicIdList = topicRankQueryService.listTopicIdsInRank(TopicAttribute.ACTIVITY_TIME, pageNo, pageSize); List topicList = new ArrayList<>(); for (Long topicId : topicIdList) { Topic topic = getTopicWithUserTagIds(topicId); if (topic != null) { //设置浏览量 topicSpQueryService.setAttrIntoTopic(topic, TopicAttribute.PAGE_VIEW); topicList.add(topic); } } return new MyPage<>(topicRankQueryService.countTopicIdsInRank(TopicAttribute.ACTIVITY_TIME), topicList); } } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/service/topic/Impl/TopicInfoWriteServiceImpl.java ================================================ package com.acimage.community.service.topic.Impl; import cn.hutool.core.util.StrUtil; import com.acimage.common.global.consts.FileFormatConstants; import com.acimage.common.global.enums.ServiceType; import com.acimage.common.model.Index.TopicIndex; import com.acimage.common.model.domain.community.CmtyUser; import com.acimage.common.model.domain.community.Topic; import com.acimage.common.model.domain.community.TopicHtml; import com.acimage.common.model.mq.dto.SyncImagesUpdateDto; import com.acimage.common.utils.*; import com.acimage.common.utils.common.ListUtils; import com.acimage.common.utils.minio.MinioUtils; import com.acimage.common.utils.redis.RedisUtils; import com.acimage.community.global.consts.CoverImageConstants; import com.acimage.common.global.consts.StorePrefixConstants; import com.acimage.community.global.consts.TopicKeyConstants; import com.acimage.community.global.enums.TopicAttribute; import com.acimage.community.listener.event.TopicEvent; import com.acimage.community.model.request.TopicAddReq; import com.acimage.community.model.request.TopicModifyHtmlReq; import com.acimage.community.mq.producer.syncImagesMqProducer; import com.acimage.community.mq.producer.SyncEsMqProducer; import com.acimage.community.service.cmtyuser.CmtyUserWriteService; import com.acimage.community.service.comment.CommentWriteService; import com.acimage.community.service.star.StarWriteService; import com.acimage.community.service.tag.TagQueryService; import com.acimage.community.service.tag.TagTopicWriteService; import com.acimage.community.service.topic.*; import com.acimage.common.global.context.UserContext; import com.acimage.common.global.exception.BusinessException; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; import javax.annotation.Resource; import java.io.InputStream; import java.util.*; import java.util.concurrent.TimeUnit; @Slf4j @Service public class TopicInfoWriteServiceImpl implements TopicInfoWriteService { @Autowired StarWriteService starWriteService; @Autowired CommentWriteService commentWriteService; @Autowired TopicQueryService topicQueryService; @Autowired TopicSpAttrWriteService topicSpAttrWriteService; @Autowired TopicRankWriteService topicRankWriteService; @Autowired TopicWriteService topicWriteService; @Autowired TagQueryService tagQueryService; @Autowired syncImagesMqProducer syncImagesMqProducer; @Autowired SyncEsMqProducer syncEsMqProducer; @Autowired CmtyUserWriteService cmtyUserWriteService; @Autowired TopicHtmlWriteService topicHtmlWriteService; @Autowired TopicHtmlQueryService topicHtmlQueryService; @Autowired TagTopicWriteService tagTopicWriteService; @Autowired MinioUtils minioUtils; @Autowired RedisUtils redisUtils; @Resource ApplicationContext applicationContext; @Override public long saveTopicInfo(TopicAddReq topicAddReq, MultipartFile coverImage) { String publishedTitle = redisUtils.getForString(TopicKeyConstants.STRINGKP_PUBLISHED_TOPIC_TITLE); if (publishedTitle != null && publishedTitle.equals(topicAddReq.getTitle())) { log.warn("user:{}重复发表话题 title:{}", UserContext.getUsername(), topicAddReq.getTitle()); throw new BusinessException("已经发表过该话题了,请刷新"); } //生成id long topicId = IdGenerator.getSnowflakeNextId(); Date now = new Date(); String suffix = String.format("%s.%s", topicId, FileFormatConstants.WEBP); String url = minioUtils.generateBaseUrl(StorePrefixConstants.COVER_IMAGE, now, suffix); //图片压缩 InputStream inputStream = ImageUtils.compressAsFixedWebpImage(coverImage, CoverImageConstants.WIDTH, CoverImageConstants.HEIGHT, CoverImageConstants.LIMIT_COMPRESS_SIZE); String coverImageUrl = minioUtils.upload(inputStream, url, FileFormatConstants.WEBP_CONTENT_TYPE); //过滤标题 String filterTile = SensitiveWordUtils.filter(topicAddReq.getTitle()); //过滤内容 String text = HtmlUtils.html2Text(topicAddReq.getHtml()); String content = SensitiveWordUtils.filter(text); //提取前200个字作为文本内容 String subContent = StrUtil.subPre(content, Topic.CONTENT_MAX); //转化 Topic topic = Topic.builder() .id(topicId) .title(filterTile) .content(subContent) .coverImageUrl(coverImageUrl) .createTime(now) .updateTime(now) .activityTime(now) .userId(UserContext.getUserId()) .categoryId(topicAddReq.getCategoryId()) .build(); if (UserContext.getUserId() != null) { CmtyUser user = new CmtyUser(); user.setUsername(UserContext.getUsername()); user.setPhotoUrl(UserContext.getPhotoUrl()); user.setId(UserContext.getUserId()); topic.setUser(user); } //检查标签是否存在 List noRepeatTagIds = ListUtils.removeRepeat(Arrays.asList(topicAddReq.getTagIds())); tagQueryService.checkAndListTags(noRepeatTagIds); //保存topic topicWriteService.save(topic); //保存话题html topicHtmlWriteService.save(topicId, topicAddReq.getHtml()); //保存标签 tagTopicWriteService.save(topicId, noRepeatTagIds); //更新活跃排行榜 topicRankWriteService.updateRank(TopicAttribute.ACTIVITY_TIME, topicId, now.getTime()); //获取话题内的站内图片链接 List newImageUrlList = HtmlUtils.getInnerImageUrls(topicAddReq.getHtml()); SyncImagesUpdateDto updateDto = SyncImagesUpdateDto.builder() .addImageUrls(newImageUrlList) .topicId(topicId) .serviceType(ServiceType.ADD) .build(); //同步到es TopicIndex topicIndex = TopicIndex.from(topic); topicIndex.setTagIds(noRepeatTagIds); //设置完整的content topicIndex.setContent(content); topicIndex.setStarCount(0); topicIndex.setCommentCount(0); topicIndex.setPageView(0); syncEsMqProducer.sendAddMessage(topicIndex); //发送到mq,用于以图识图 syncImagesMqProducer.sendSyncImagesMessage(updateDto); TopicEvent topicEvent = new TopicEvent(this, UserContext.getUserId(), topicId); applicationContext.publishEvent(topicEvent); //保存,用于幂等校验 long timeout = 10L; redisUtils.setAsString(TopicKeyConstants.STRINGKP_PUBLISHED_TOPIC_TITLE + UserContext.getUserId(), topicAddReq.getTitle(), timeout, TimeUnit.SECONDS); return topicId; } @Override public void removeTopicInfo(long topicId) { Topic topic = topicQueryService.getTopic(topicId); if (topic == null) { log.error("话题不存在 话题:{} 用户:{}", topicId, UserContext.getUsername()); throw new BusinessException("话题不存在~~"); } if (!UserContext.getUserId().equals(topic.getUserId())) { log.error("非法删除:用户非话题主人 话题:{} 用户:{}", topicId, UserContext.getUsername()); throw new BusinessException("非法操作!话题不属于你"); } this.removeTopicInfoWithoutVerification(topicId); // //删除star // starWriteService.removeStars(topicId); // //删除评论 // commentWriteService.removeComments(topicId); // //删除话题 // topicHtmlWriteService.remove(topicId); // topicWriteService.remove(topicId); // //删除标签 // tagTopicWriteService.remove(topicId); // //删除相关属性 // topicSpAttrWriteService.removeAttributes(topicId); // //更新用户统计数据 // cmtyUserWriteService.updateTopicCountByIncrement(userId, -1); // //发送删除图片的消息 // removeTopicImagesMqProducer.sendRemoveTopicMessage(topicId); // //同步es数据 // syncEsMqProducer.sendDeleteMessage(Long.toString(topicId), TopicIndex.class); // //同步到以图识图 // SyncImagesUpdateDto updateDto = SyncImagesUpdateDto.builder() // .topicId(topicId) // .serviceType(ServiceType.DELETE) // .build(); // syncImagesMqProducer.sendHashImagesMessage(updateDto); } @Override public void removeTopicInfoWithoutVerification(long topicId) { Topic topic = topicQueryService.getTopic(topicId); if (topic == null) { log.error("话题不存在 话题:{} 用户:{}", topicId, UserContext.getUsername()); throw new BusinessException("话题不存在~~"); } long userId = topic.getUserId(); //删除star starWriteService.removeStars(topicId); //删除评论 commentWriteService.removeComments(topicId); //删除话题 topicHtmlWriteService.remove(topicId); topicWriteService.remove(topicId); //删除标签 tagTopicWriteService.remove(topicId); //删除相关属性 topicSpAttrWriteService.removeAttributes(topicId); //更新用户统计数据 cmtyUserWriteService.updateTopicCountByIncrement(userId, -1); //同步es数据 syncEsMqProducer.sendDeleteMessage(Long.toString(topicId), TopicIndex.class); //同步图片 SyncImagesUpdateDto updateDto = SyncImagesUpdateDto.builder() .topicId(topicId) .serviceType(ServiceType.DELETE) .build(); syncImagesMqProducer.sendSyncImagesMessage(updateDto); } @Override public void updateHtml(TopicModifyHtmlReq modifyReq) { long topicId = modifyReq.getId(); Topic topic = topicQueryService.getTopic(topicId); if (topic == null) { log.error("话题不存在 话题:{} 用户:{}", topicId, UserContext.getUsername()); throw new BusinessException("话题不存在~~"); } if (!UserContext.getUserId().equals(topic.getUserId())) { log.error("异常修改:用户非话题主人 话题:{} 用户:{}", topicId, UserContext.getUsername()); throw new BusinessException("非法操作!话题不属于你"); } //过滤内容 String text = HtmlUtils.html2Text(modifyReq.getHtml()); String content = SensitiveWordUtils.filter(text); //提取前200个字作为文本内容 String subContent = StrUtil.subPre(content, Topic.CONTENT_MAX); //获取变化的图片链接 SyncImagesUpdateDto updateDto = null; TopicHtml topicHtml = topicHtmlQueryService.getTopicHtml(topicId); if (topicHtml != null) { List oldUrls = HtmlUtils.getInnerImageUrls(topicHtml.getHtml()); List newUrls = HtmlUtils.getInnerImageUrls(modifyReq.getHtml()); List addImageUrls = ListUtils.differenceSetOfV2(newUrls, oldUrls); List removeImageUrls = ListUtils.differenceSetOfV2(oldUrls, newUrls); updateDto = SyncImagesUpdateDto.builder() .topicId(topicId) .addImageUrls(addImageUrls) .removeImageUrls(removeImageUrls) .serviceType(ServiceType.UPDATE) .build(); } topicWriteService.updateContent(topicId, subContent); topicHtmlWriteService.update(topicId, SensitiveWordUtils.filter(modifyReq.getHtml())); TopicIndex topicIndex = TopicIndex.builder() .updateTime(new Date()) .content(content) .id(topicId) .build(); List columns = LambdaUtils.columnsFrom(TopicIndex::getContent, TopicIndex::getUpdateTime); syncEsMqProducer.sendUpdateMessage(topicIndex, columns); //同步图片哈希 if (updateDto != null) { syncImagesMqProducer.sendSyncImagesMessage(updateDto); } } } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/service/topic/Impl/TopicPreheatServiceImpl.java ================================================ package com.acimage.community.service.topic.Impl; import com.acimage.common.model.domain.community.Topic; import com.acimage.common.utils.LambdaUtils; import com.acimage.common.utils.redis.RedisUtils; import com.acimage.community.dao.TopicDao; import com.acimage.community.service.topic.TopicPreheatService; import com.acimage.community.service.topic.TopicRankWriteService; import com.acimage.community.global.consts.TopicKeyConstants; import com.acimage.community.global.enums.TopicAttribute; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; import java.util.concurrent.TimeUnit; @Service public class TopicPreheatServiceImpl implements TopicPreheatService { @Autowired TopicDao topicDao; @Autowired TopicRankWriteService topicRankWriteService; @Autowired RedisUtils redisUtils; @Override public void preheatTopicsOrderBy(TopicAttribute attr, int rankSize, int cacheSize, long timeout, TimeUnit timeUnit) { String underlineColumnName = attr.toUnderlineColumnName(); LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); qw.last(String.format(" order by %s desc limit %d", underlineColumnName, rankSize)); List hotTopics = topicDao.selectList(qw); // List hotTopics = topicDao.selectTopicsWithUserOrderBy(underlineColumnName, rankSize); for (int i = 0; i < hotTopics.size(); i++) { Topic topic = hotTopics.get(i); topicRankWriteService.updateRank(attr, topic); // // if (i < cacheSize) { // String key = TopicKeyConstants.HASHKP_TOPIC + topic.getId(); // redisUtils.setObjectForHash(key, topic, timeout, timeUnit); // } } } } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/service/topic/Impl/TopicQueryServiceImpl.java ================================================ package com.acimage.community.service.topic.Impl; import cn.hutool.core.collection.CollectionUtil; import com.acimage.common.global.context.UserContext; import com.acimage.common.model.domain.community.Topic; import com.acimage.common.redis.annotation.QueryRedis; import com.acimage.common.redis.enums.DataType; import com.acimage.common.utils.redis.RedisUtils; import com.acimage.community.dao.TopicDao; import com.acimage.community.service.topic.TopicQueryService; import com.acimage.community.global.consts.TopicKeyConstants; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.List; @Slf4j @Service public class TopicQueryServiceImpl implements TopicQueryService { @Autowired TopicDao topicDao; @QueryRedis(expire = 11L, keyPrefix = TopicKeyConstants.HASHKP_TOPIC, dataType = DataType.HASH) @Override public Topic getTopic(long id) { Topic topic = topicDao.selectTopicWithUser(id); if (topic == null) { log.error("user:{} 查询 对象:话题{} error:话题不存在", UserContext.getUsername(), id); return null; } return topic; } @Override public List listTopicWithUser(List ids) { if(CollectionUtil.isEmpty(ids)){ return new ArrayList<>(); } return topicDao.selectTopicsWithUserByIds(ids); } @Override public List listTopicsByIds(List ids) { return topicDao.selectBatchIds(ids); } } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/service/topic/Impl/TopicRankQueryServiceImpl.java ================================================ package com.acimage.community.service.topic.Impl; import cn.hutool.core.lang.Pair; import com.acimage.common.utils.redis.RedisUtils; import com.acimage.common.utils.common.ListUtils; import com.acimage.common.utils.common.PageUtils; import com.acimage.community.service.topic.TopicRankQueryService; import com.acimage.community.global.enums.TopicAttribute; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import javax.validation.constraints.NotNull; import java.util.List; import java.util.stream.Collectors; @Service public class TopicRankQueryServiceImpl implements TopicRankQueryService { @Autowired RedisUtils redisUtils; @Override public List listTopicIdsInRank(TopicAttribute topicAttribute, int pageNo, int pageSize) { List> topicIdAndScores = listTopicIdWithScoresInRank(topicAttribute, pageNo, pageSize); return ListUtils.extractKeyFrom(topicIdAndScores); } @Override public List listRandomTopicIdsInRank(TopicAttribute topicAttribute, int size) { return redisUtils.randomMembersForZSet(topicAttribute.zSetKey(),size) .stream() .map(Long::parseLong) .collect(Collectors.toList()); } @Override public List> listTopicIdWithScoresInRank(TopicAttribute topicAttribute, int pageNo, int pageSize) { int startIndex = PageUtils.startIndexOf(pageNo, pageSize); int endIndex = PageUtils.endIndexOf(pageNo, pageSize); String zSetKey = topicAttribute.zSetKey(); if (zSetKey == null) { return null; } List> topicIdStringAndScores = redisUtils.reverseRangeWithScoreForZSet(zSetKey, startIndex, endIndex); return ListUtils.convertToLongDoublePairFrom(topicIdStringAndScores); } @Override public Integer countTopicIdsInRank(@NotNull TopicAttribute topicAttribute) { String rankingListKey = topicAttribute.zSetKey(); Long count = redisUtils.sizeForZSet(rankingListKey); if (count == null) { return null; } else { return count.intValue(); } } } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/service/topic/Impl/TopicRankWriteServiceImpl.java ================================================ package com.acimage.community.service.topic.Impl; import com.acimage.common.model.domain.community.Topic; import com.acimage.common.utils.redis.RedisUtils; import com.acimage.community.service.topic.TopicRankWriteService; import com.acimage.community.global.consts.TopicKeyConstants; import com.acimage.community.global.enums.TopicAttribute; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import javax.validation.constraints.NotNull; import java.util.Date; @Service public class TopicRankWriteServiceImpl implements TopicRankWriteService { @Autowired RedisUtils redisUtils; @Override public void updateRank(TopicAttribute attr, long topicId, double newScore) { String key = attr.zSetKey(); if (key == null) { return; } //更新对应排行榜 redisUtils.addForZSet(key, Long.toString(topicId), newScore); } @Override public void updateRank(TopicAttribute attr, @NotNull Topic topic) { if (topic == null) { return; } if (attr == TopicAttribute.ACTIVITY_TIME) { Date activityTime = redisUtils.getObjectFromString(TopicKeyConstants.STRINGKP_TOPIC_ACTIVITY_TIME + topic.getId(), Date.class); if (activityTime == null) { activityTime = topic.getActivityTime(); } //更新对应排行榜 redisUtils.addForZSet(attr.zSetKey(), topic.getId().toString(), activityTime.getTime()); } else if (attr == TopicAttribute.PAGE_VIEW) { String logKey = TopicKeyConstants.LOGKP_TOPIC_PV + topic.getId(); int increment = redisUtils.sizeForHyperLogLog(logKey).intValue(); int latestPv = topic.getPageView() + increment; //更新对应排行榜 redisUtils.addForZSet(attr.zSetKey(), topic.getId().toString(), latestPv); } else if (attr == TopicAttribute.COMMENT_COUNT || attr == TopicAttribute.STAR_COUNT) { String key = attr.keyPrefix() + topic.getId(); Integer increment = redisUtils.getForString(key, Integer.class); int baseValue = (Integer) attr.toGetFunction().apply(topic); if (increment != null) { baseValue += increment; } //更新对应排行榜 redisUtils.addForZSet(attr.zSetKey(), topic.getId().toString(), baseValue); } } } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/service/topic/Impl/TopicSpAttrQueryServiceImpl.java ================================================ package com.acimage.community.service.topic.Impl; import com.acimage.common.model.domain.community.Topic; import com.acimage.common.utils.redis.RedisUtils; import com.acimage.community.service.topic.TopicQueryService; import com.acimage.community.service.topic.TopicSpAttrQueryService; import com.acimage.community.global.consts.TopicKeyConstants; import com.acimage.community.global.enums.TopicAttribute; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.Arrays; import java.util.Date; import java.util.List; @Service public class TopicSpAttrQueryServiceImpl implements TopicSpAttrQueryService { @Autowired RedisUtils redisUtils; @Autowired TopicQueryService topicQueryService; @Override public Integer getPageView(long topicId) { //获取基础浏览量 Topic topic = topicQueryService.getTopic(topicId); if (topic == null) { return null; } int pageView = topic.getPageView(); //从redis中获取新增浏览量 String topicPvLogKey = TopicKeyConstants.LOGKP_TOPIC_PV + topicId; Long pvIncrement = redisUtils.sizeForHyperLogLog(topicPvLogKey); int increment = pvIncrement == null ? 0 : pvIncrement.intValue(); return pageView + increment; } @Override public Date getActivityTime(long topicId) { //先从redis获取 Date activityTime = redisUtils.getObjectFromString(TopicKeyConstants.STRINGKP_TOPIC_ACTIVITY_TIME + topicId, Date.class); if (activityTime != null) { return activityTime; } Topic topic = topicQueryService.getTopic(topicId); if (topic == null) { return null; } return topic.getActivityTime(); } @Override public Integer getStarCount(long topicId) { //获取基础浏览量 Topic topic = topicQueryService.getTopic(topicId); if (topic == null) { return null; } int starCount = topic.getStarCount(); //从redis中获取新增浏览量 Long starCountIncrement = redisUtils.getForString(TopicAttribute.STAR_COUNT.keyPrefix() + topicId, Long.class); int increment = starCountIncrement == null ? 0 : starCountIncrement.intValue(); return starCount + increment; } @Override public Integer getCommentCount(long topicId) { //获取基础浏览量 Topic topic = topicQueryService.getTopic(topicId); if (topic == null) { return null; } int commentCount = topic.getCommentCount(); //从redis中获取新增浏览量 Long commentIncrement = redisUtils.getForString(TopicAttribute.COMMENT_COUNT.keyPrefix() + topicId, Long.class); int increment = commentIncrement == null ? 0 : commentIncrement.intValue(); return commentCount + increment; } @Override public void setAttrIntoTopic(Topic topic, TopicAttribute... attrs) { if (topic == null) { return; } List typeList = Arrays.asList(attrs); long topicId = topic.getId(); if (typeList.contains(TopicAttribute.PAGE_VIEW)) { Integer pv = this.getPageView(topicId); topic.setPageView(pv); // topicSpAttrWriteService.updateRank(TopicAttribute.PAGE_VIEW,topicId,pv); } if (typeList.contains(TopicAttribute.STAR_COUNT)) { Integer starCount = this.getStarCount(topicId); topic.setStarCount(starCount); // topicSpAttrWriteService.updateRank(TopicAttribute.STAR_COUNT,topicId,starCount); } if (typeList.contains(TopicAttribute.COMMENT_COUNT)) { Integer commentCount = this.getCommentCount(topicId); topic.setCommentCount(commentCount); // topicSpAttrWriteService.updateRank(TopicAttribute.COMMENT_COUNT,topicId,commentCount); } if (typeList.contains(TopicAttribute.ACTIVITY_TIME)) { Date activityTime = this.getActivityTime(topicId); topic.setActivityTime(activityTime); // topicSpAttrWriteService.updateRank(TopicAttribute.ACTIVITY_TIME,topicId,activityTime.getTime()); } } } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/service/topic/Impl/TopicSpAttrWriteServiceImpl.java ================================================ package com.acimage.community.service.topic.Impl; import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.lang.Pair; import com.acimage.common.model.Index.TopicIndex; import com.acimage.common.model.domain.community.Topic; import com.acimage.common.utils.LambdaUtils; import com.acimage.common.utils.redis.RedisUtils; import com.acimage.common.utils.common.PairUtils; import com.acimage.community.dao.TopicDao; import com.acimage.community.mq.producer.SyncEsMqProducer; import com.acimage.community.service.topic.TopicRankWriteService; import com.acimage.community.service.topic.TopicSpAttrQueryService; import com.acimage.community.service.topic.TopicSpAttrWriteService; import com.acimage.community.global.consts.TopicKeyConstants; import com.acimage.community.global.enums.TopicAttribute; import com.baomidou.mybatisplus.core.toolkit.support.SFunction; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.Date; import java.util.List; @Slf4j @Service public class TopicSpAttrWriteServiceImpl implements TopicSpAttrWriteService { @Autowired RedisUtils redisUtils; @Autowired TopicDao topicDao; @Autowired TopicSpAttrQueryService topicSpAttrQueryService; @Autowired TopicRankWriteService topicRankWriteService; @Autowired SyncEsMqProducer syncEsMqProducer; @Override public void removeAttributes(long topicId) { for (TopicAttribute attr : TopicAttribute.values()) { //删除保存属性最新变化的数据 redisUtils.delete(attr.keyPrefix() + topicId); //删除set中记录的id redisUtils.removeForSet(attr.setKeyForRecordingId(), Long.toString(topicId)); //删除排行榜中的id redisUtils.removeForZSet(attr.zSetKey(), topicId); } } @Override public void updateStarCountByIncrement(List topicIds, List increments) { batchUpdateByIncrements(TopicAttribute.STAR_COUNT, topicIds, increments); } @Override public void updateCommentCountByIncrement(List topicIds, List increments) { batchUpdateByIncrements(TopicAttribute.COMMENT_COUNT, topicIds, increments); } @Override public void updatePageViewByIncrement(List topicIds, List increments) { batchUpdateByIncrements(TopicAttribute.PAGE_VIEW, topicIds, increments); } private void batchUpdateByIncrements(TopicAttribute attr, List topicIds, List increments) { String underlineColumnName = attr.toUnderlineColumnName(); List> pairList = PairUtils.combine(topicIds, increments); if (!CollectionUtil.isEmpty(pairList)) { topicDao.batchUpdateColumnByIncrement(underlineColumnName, pairList); } } private void updateByIncrement(SFunction column, long topicId, int increment) { String underlineName = LambdaUtils.underlineColumnNameOf(column); topicDao.updateColumnByIncrement(underlineName, topicId, increment); } @Override public void updateBatchActivityTime(List topicIds, List activityTimes) { List> pairList = PairUtils.combine(topicIds, activityTimes); if (!CollectionUtil.isEmpty(pairList)) { topicDao.batchUpdateActivityTime(pairList); } } @Override public void changeActivityTime(long topicId, Date date) { //更新数据库 topicDao.updateActivityTime(topicId, date); String key = TopicKeyConstants.HASHKP_TOPIC + topicId; String column = LambdaUtils.columnOf(Topic::getActivityTime); //保存最新值 redisUtils.setIfPresentForFieldKey(key, column, Long.toString(date.getTime())); //更新排行榜 topicRankWriteService.updateRank(TopicAttribute.ACTIVITY_TIME, topicId, date.getTime()); //同步到es TopicIndex topicIndex = TopicIndex.builder() .activityTime(date) .id(topicId) .build(); List columns = LambdaUtils.columnsFrom(TopicIndex::getActivityTime); syncEsMqProducer.sendUpdateMessage(topicIndex, columns); } @Override public void increaseStarCount(long topicId, int increment) { if (increment != 0) { //改数据库 this.updateByIncrement(Topic::getStarCount, topicId, increment); //同步到redis String column = LambdaUtils.columnOf(Topic::getStarCount); String key = TopicKeyConstants.HASHKP_TOPIC + topicId; //获取最新值更新排行榜 Long starCount = redisUtils.incrementIfPresentForFieldKey(key, column, increment); if (starCount == null) { Integer cnt = topicSpAttrQueryService.getStarCount(topicId); if (cnt == null) { return; } starCount = cnt.longValue(); } //更新排行榜 topicRankWriteService.updateRank(TopicAttribute.STAR_COUNT, topicId, starCount); //同步到es TopicIndex topicIndex = TopicIndex.builder() .starCount(starCount.intValue()) .id(topicId) .build(); List columns = LambdaUtils.columnsFrom(TopicIndex::getStarCount); syncEsMqProducer.sendUpdateMessage(topicIndex, columns); } } @Override public void increaseCommentCount(long topicId, int increment) { if (increment != 0) { //改数据库 this.updateByIncrement(Topic::getCommentCount, topicId, increment); //同步到redis String column = LambdaUtils.columnOf(Topic::getCommentCount); String key = TopicKeyConstants.HASHKP_TOPIC + topicId; //获取最新值更新排行榜 Long commentCount = redisUtils.incrementIfPresentForFieldKey(key, column, increment); if (commentCount == null) { Integer cnt = topicSpAttrQueryService.getCommentCount(topicId); if (cnt == null) { return; } commentCount = cnt.longValue(); } topicRankWriteService.updateRank(TopicAttribute.COMMENT_COUNT, topicId, commentCount); //同步到es TopicIndex topicIndex = TopicIndex.builder() .commentCount(commentCount.intValue()) .id(topicId) .build(); List columns = LambdaUtils.columnsFrom(TopicIndex::getCommentCount); syncEsMqProducer.sendUpdateMessage(topicIndex, columns); } } } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/service/topic/Impl/TopicWriteServiceImpl.java ================================================ package com.acimage.community.service.topic.Impl; import com.acimage.common.global.context.UserContext; import com.acimage.common.model.Index.TopicIndex; import com.acimage.common.model.domain.community.Topic; import com.acimage.common.utils.LambdaUtils; import com.acimage.common.utils.SensitiveWordUtils; import com.acimage.common.utils.redis.RedisUtils; import com.acimage.community.dao.TopicDao; import com.acimage.community.model.request.TopicModifyHtmlReq; import com.acimage.community.mq.producer.SyncEsMqProducer; import com.acimage.community.service.topic.TopicSpAttrWriteService; import com.acimage.community.service.topic.TopicWriteService; import com.acimage.community.global.consts.TopicKeyConstants; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.Date; import java.util.List; @Slf4j @Service public class TopicWriteServiceImpl implements TopicWriteService { @Autowired TopicDao topicDao; @Autowired RedisUtils redisUtils; @Autowired SyncEsMqProducer syncEsMqProducer; @Autowired TopicSpAttrWriteService topicSpAttrWriteService; @Override public void save(Topic topic) { topicDao.insert(topic); log.info("用户:{} 插入 话题{}", UserContext.getUsername(), topic); } @Override public void remove(long id) { topicDao.deleteById(id); //删除redis数据 redisUtils.delete(TopicKeyConstants.HASHKP_TOPIC + id); log.info("用户:{} 删除 话题{}", UserContext.getUsername(), id); } @Override public void update(long id, String title, String content) { Topic topic = new Topic(); topic.setTitle(title); topic.setContent(content); topic.setUpdateTime(new Date()); topicDao.updateById(topic); // LambdaUpdateWrapper qw=new LambdaUpdateWrapper<>(); // qw.set(Topic::getContent,content) // .set(Topic::getTitle,title) // .eq(Topic::getId,id); // topicDao.update(null,qw); // topicDao.updateTopic(id, title, content); //删除redis数据 redisUtils.delete(TopicKeyConstants.HASHKP_TOPIC + id); log.info("用户:{} 修改 话题{}", UserContext.getUsername(), id); } @Override public void updateTitle(long id, String title) { log.info("user:{} 修改 话题标题{} title:{}", UserContext.getUsername(), id, title); String filterTile = SensitiveWordUtils.filter(title); LambdaUpdateWrapper uw = new LambdaUpdateWrapper<>(); uw.eq(Topic::getId, id) .set(Topic::getTitle, filterTile) .set(Topic::getUpdateTime, new Date()); topicDao.update(null, uw); //删除缓存 redisUtils.delete(TopicKeyConstants.HASHKP_TOPIC + id); //更新话题活跃时间 topicSpAttrWriteService.changeActivityTime(id, new Date()); //同步到es TopicIndex topicIndex = TopicIndex.builder() .id(id) .title(filterTile) .build(); topicIndex.setTitle(filterTile); List columns= LambdaUtils.columnsFrom(TopicIndex::getTitle); syncEsMqProducer.sendUpdateMessage(topicIndex,columns); } @Deprecated @Override public void updateContent(TopicModifyHtmlReq topicModifyHtmlReq) { long id = topicModifyHtmlReq.getId(); String content = topicModifyHtmlReq.getHtml(); log.info("user:{} 修改 话题标题{} content:{}", UserContext.getUsername(), id, content); LambdaUpdateWrapper uw = new LambdaUpdateWrapper<>(); uw.eq(Topic::getId, id) .set(Topic::getContent, content) .set(Topic::getUpdateTime, new Date()); topicDao.update(null, uw); //删除缓存 redisUtils.delete(TopicKeyConstants.HASHKP_TOPIC + id); //更新话题活跃时间 topicSpAttrWriteService.changeActivityTime(id, new Date()); } @Override public void updateContent(long id, String content) { log.info("user:{} 修改 话题标题{} content:{}", UserContext.getUsername(), id, content); LambdaUpdateWrapper uw = new LambdaUpdateWrapper<>(); uw.eq(Topic::getId, id) .set(Topic::getContent, content) .set(Topic::getUpdateTime, new Date()); topicDao.update(null, uw); //删除缓存 redisUtils.delete(TopicKeyConstants.HASHKP_TOPIC + id); //更新话题活跃时间 topicSpAttrWriteService.changeActivityTime(id, new Date()); } } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/service/topic/TopicEsSearchService.java ================================================ package com.acimage.community.service.topic; import com.acimage.common.model.domain.community.Topic; import com.acimage.common.model.page.MyPage; import com.acimage.community.model.request.TopicQueryByCategoryIdReq; import com.acimage.community.model.request.TopicQueryBySortReq; import com.acimage.community.model.request.TopicQueryByTagIdReq; import com.acimage.community.model.request.TopicSearchReq; import java.util.List; public interface TopicEsSearchService { List searchSimilar(long topicId, int size); List searchSimilarByTitle(long topicId,String title, int size); MyPage searchByTagId(TopicQueryByTagIdReq queryReq); MyPage searchBySort(TopicQueryByCategoryIdReq queryReq); MyPage searchBySort(TopicQueryBySortReq queryReq); MyPage search(TopicSearchReq topicSearchReq); } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/service/topic/TopicHtmlQueryService.java ================================================ package com.acimage.community.service.topic; import com.acimage.common.model.domain.community.TopicHtml; public interface TopicHtmlQueryService { TopicHtml getTopicHtml(long topicId); } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/service/topic/TopicHtmlWriteService.java ================================================ package com.acimage.community.service.topic; import com.acimage.common.model.domain.community.TopicHtml; public interface TopicHtmlWriteService { TopicHtml save(long topicId, String html); void remove(long topicId); void update(long topicId, String html); } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/service/topic/TopicInfoQueryService.java ================================================ package com.acimage.community.service.topic; import com.acimage.common.model.domain.community.Topic; import com.acimage.common.model.page.MyPage; import com.acimage.community.global.enums.TopicAttribute; import com.acimage.community.model.vo.TopicInfoVo; import java.util.List; public interface TopicInfoQueryService { Topic getTopicWithUserTagIds(long topicId); TopicInfoVo getTopicInfoAndFirstCommentPage(long topicId); MyPage pageUserTopicsInfoOrderByCreateTime(long userId, int pageNo, int pageSize); List listTopicsInfoSortBy(TopicAttribute attribute, int pageNo, int pageSize); List listRandomTopicsInRank(int size); MyPage pageTopicsInfoRankByActivityTime(int pageNo, int pageSize); } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/service/topic/TopicInfoWriteService.java ================================================ package com.acimage.community.service.topic; import com.acimage.community.model.request.TopicAddReq; import com.acimage.community.model.request.TopicModifyHtmlReq; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; import java.io.IOException; public interface TopicInfoWriteService { @Transactional(rollbackFor = {Exception.class}) long saveTopicInfo(TopicAddReq topicAddReq, MultipartFile coverImage); @Transactional(rollbackFor = {Exception.class}) void removeTopicInfo(long topicId); @Transactional(rollbackFor = {Exception.class}) void removeTopicInfoWithoutVerification(long topicId); @Transactional void updateHtml(TopicModifyHtmlReq modifyReq); } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/service/topic/TopicPreheatService.java ================================================ package com.acimage.community.service.topic; import com.acimage.community.global.enums.TopicAttribute; import java.util.concurrent.TimeUnit; public interface TopicPreheatService { void preheatTopicsOrderBy(TopicAttribute attr, int rankSize, int cacheSize, long timeout, TimeUnit timeUnit); } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/service/topic/TopicQueryService.java ================================================ package com.acimage.community.service.topic; import com.acimage.common.model.domain.community.Topic; import java.util.List; public interface TopicQueryService { Topic getTopic(long id); List listTopicWithUser(List ids); List listTopicsByIds(List ids); } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/service/topic/TopicRankQueryService.java ================================================ package com.acimage.community.service.topic; import cn.hutool.core.lang.Pair; import com.acimage.community.global.enums.TopicAttribute; import java.util.List; /** * 负责话题的 浏览量、收藏数、评论数、最新活跃时间 相关的服务 */ public interface TopicRankQueryService { List listTopicIdsInRank(TopicAttribute topicAttribute, int pageNo, int pageSize); List listRandomTopicIdsInRank(TopicAttribute topicAttribute, int size); List> listTopicIdWithScoresInRank(TopicAttribute topicAttribute, int pageNo, int pageSize); Integer countTopicIdsInRank(TopicAttribute topicAttribute); } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/service/topic/TopicRankWriteService.java ================================================ package com.acimage.community.service.topic; import com.acimage.common.model.domain.community.Topic; import com.acimage.community.global.enums.TopicAttribute; import javax.validation.constraints.NotNull; public interface TopicRankWriteService { void updateRank(TopicAttribute attr, long topicId, double newScore); void updateRank(TopicAttribute attr, @NotNull Topic topic); } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/service/topic/TopicSpAttrQueryService.java ================================================ package com.acimage.community.service.topic; import com.acimage.common.model.domain.community.Topic; import com.acimage.community.global.enums.TopicAttribute; import java.util.Date; public interface TopicSpAttrQueryService { Integer getPageView(long topicId); Date getActivityTime(long topicId); Integer getStarCount(long topicId); Integer getCommentCount(long topicId); void setAttrIntoTopic(Topic topic, TopicAttribute... attrs); } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/service/topic/TopicSpAttrWriteService.java ================================================ package com.acimage.community.service.topic; import java.util.Date; import java.util.List; public interface TopicSpAttrWriteService { void removeAttributes(long topicId); void updateStarCountByIncrement(List topicIds,List increments); void updateCommentCountByIncrement(List topicIds,List increments); void updatePageViewByIncrement(List topicIds,List increments); void updateBatchActivityTime(List topicIds, List activityTimes); void changeActivityTime(long topicId, Date date); void increaseStarCount(long topicId, int increment); void increaseCommentCount(long topicId, int increment); } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/service/topic/TopicWriteService.java ================================================ package com.acimage.community.service.topic; import com.acimage.common.model.domain.community.Topic; import com.acimage.community.model.request.TopicModifyHtmlReq; public interface TopicWriteService { void save(Topic topic); void remove(long id); void update(long id,String title,String content); void updateTitle(long id, String title); void updateContent(TopicModifyHtmlReq topicModifyHtmlReq); void updateContent(long id, String content); } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/utils/RsaUtils.java ================================================ package com.acimage.community.utils; import cn.hutool.crypto.asymmetric.KeyType; import cn.hutool.crypto.asymmetric.RSA; public class RsaUtils { private static final String privateKey; private static final String publicKey; static { RSA rsa = new RSA(); privateKey=rsa.getPrivateKeyBase64(); publicKey =rsa.getPublicKeyBase64(); } public static String decrypt(String privateKeyBase64,String encryptBase64){ RSA rsa=new RSA(privateKeyBase64,null); return rsa.decryptStr(encryptBase64, KeyType.PrivateKey); } public static String getPrivateKey(){ return privateKey; } public static String getPublicKey(){ return publicKey; } } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/web/controller/CategoryQueryController.java ================================================ package com.acimage.community.web.controller; import com.acimage.common.model.domain.community.Category; import com.acimage.common.redis.annotation.RequestLimit; import com.acimage.common.redis.enums.LimitTarget; import com.acimage.common.result.Result; import com.acimage.community.service.categoty.CategoryQueryService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.List; @RestController @Slf4j @Validated @RequestMapping("/api/community/categories/query") public class CategoryQueryController { @Autowired CategoryQueryService categoryQueryService; @GetMapping("/all") public Result> queryAllCategories() { return Result.ok(categoryQueryService.listAll()); } } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/web/controller/CommentOperateController.java ================================================ package com.acimage.community.web.controller; import com.acimage.common.global.consts.TimeConstants; import com.acimage.common.redis.annotation.RequestLimit; import com.acimage.common.redis.enums.LimitTarget; import com.acimage.common.result.Result; import com.acimage.community.model.request.CommentAddReq; import com.acimage.community.model.request.CommentModifyReq; import com.acimage.community.service.comment.CommentWriteService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import javax.validation.constraints.NotNull; import javax.validation.constraints.Positive; @RestController @Slf4j @Validated @RequestMapping("/api/community/comments/operate") public class CommentOperateController { @Autowired CommentWriteService commentWriteService; @RequestLimit(limitTimes = {1,20}, durations = {3, TimeConstants.DAY_SECONDS}, penaltyTimes = {-1,-1}, targets = {LimitTarget.IP,LimitTarget.USER}) @PostMapping public Result addComment(@Validated @RequestBody CommentAddReq commentAddReq){ String trimContent=commentAddReq.getContent().trim(); commentAddReq.setContent(trimContent); if(trimContent.length()<2){ return Result.fail("评论有效字数不能少于2个字"); } commentWriteService.saveComment(commentAddReq); log.info("评论了 {}", commentAddReq); return Result.ok("评论成功"); } @RequestLimit(limitTimes = {1,20}, durations = {3, TimeConstants.DAY_SECONDS}, penaltyTimes = {-1,-1}, targets = {LimitTarget.IP,LimitTarget.USER}) @PutMapping public Result modifyComment(@Validated @RequestBody CommentModifyReq commentModifyReq){ String trimContent=commentModifyReq.getContent().trim(); commentModifyReq.setContent(trimContent); if(trimContent.length()<2){ return Result.fail("评论有效字数不能少于2个字"); } log.info("修改了 {}", commentModifyReq); commentWriteService.updateComment(commentModifyReq); return Result.ok(); } @RequestLimit(limitTimes = {1,20}, durations = {3, TimeConstants.DAY_SECONDS}, penaltyTimes = {-1,-1}, targets = {LimitTarget.IP,LimitTarget.USER}) @DeleteMapping("/{id}") public Result deleteComment(@Positive @NotNull @PathVariable("id") Long id){ log.info("删除 评论{}",id); commentWriteService.removeComment(id); return Result.ok(); } } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/web/controller/CommentQueryController.java ================================================ package com.acimage.community.web.controller; import com.acimage.common.result.Result; import com.acimage.common.global.context.UserContext; import com.acimage.common.model.domain.community.Comment; import com.acimage.community.global.consts.PageSizeConstants; import com.acimage.community.service.comment.CommentInfoQueryService; import lombok.extern.slf4j.Slf4j; import org.hibernate.validator.constraints.Range; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import javax.validation.constraints.NotNull; import javax.validation.constraints.Positive; import java.util.List; @RestController @Slf4j @Validated @RequestMapping("/api/community/comments/query") public class CommentQueryController { @Autowired CommentInfoQueryService commentInfoQueryService; @GetMapping("/topicComments") public Result pageTopicComments(@Positive @RequestParam("topicId") Long topicId, @Range(min=1,max=100,message="页码在1-100之间") @RequestParam("pageNo") Integer pageNo) { List comments = commentInfoQueryService.pageCommentsWithUser(topicId, pageNo, PageSizeConstants.TOPIC_COMMENTS); return Result.ok(comments); } @GetMapping("/mine/{pageNo}") public Result pageMyComments(@Range(min=1,max=100,message="页码在1-100之间") @PathVariable("pageNo") Integer pageNo) { return Result.ok(commentInfoQueryService.pageCommentsWithTopicOrderByCreateTime(UserContext.getUserId(), pageNo, PageSizeConstants.ACTIVITY_COMMENTS)); } } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/web/controller/HomeCarouselQueryController.java ================================================ package com.acimage.community.web.controller; import com.acimage.common.deprecated.annotation.Authentication; import com.acimage.common.global.enums.AuthenticationType; import com.acimage.common.model.domain.community.HomeCarousel; import com.acimage.common.result.Result; import com.acimage.community.service.homecarousel.HomeCarouselQueryService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import java.util.List; @RestController @Slf4j @Validated @Authentication(type = AuthenticationType.NONE) @RequestMapping("/api/community/homeCarousels") public class HomeCarouselQueryController { @Autowired HomeCarouselQueryService homeCarouselQueryService; @GetMapping("/list") public Result> queryHomeCarousel() { List homeCarousels = homeCarouselQueryService.listAll(); return Result.ok(homeCarousels); } } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/web/controller/StarOperateController.java ================================================ package com.acimage.community.web.controller; import com.acimage.common.global.consts.TimeConstants; import com.acimage.common.redis.annotation.RequestLimit; import com.acimage.common.redis.enums.LimitTarget; import com.acimage.common.result.Result; import com.acimage.common.deprecated.annotation.Authentication; import com.acimage.common.global.context.UserContext; import com.acimage.community.service.star.StarMixQueryService; import com.acimage.community.service.star.StarQueryService; import com.acimage.community.service.star.StarWriteService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import javax.validation.constraints.Positive; @RestController @Slf4j @Validated @Authentication @RequestMapping("/api/community/stars/operate") public class StarOperateController { @Autowired StarWriteService starWriteService; @Autowired StarQueryService starQueryService; @Autowired StarMixQueryService starMixQueryService; @RequestLimit(limitTimes = {1}, durations = {3}, penaltyTimes = {-1}, targets = {LimitTarget.USER}) @PostMapping("/{topicId}") public Result addStar(@Positive @PathVariable("topicId") Long topicId) { starWriteService.saveStar(UserContext.getUserId(),topicId); return Result.ok(); } @RequestLimit(limitTimes = {1}, durations = {3}, penaltyTimes = {-1}, targets = {LimitTarget.USER}) @DeleteMapping("/{topicId}") public Result deleteStar(@Positive @PathVariable("topicId") Long topicId) { starWriteService.removeStar(UserContext.getUserId(),topicId); return Result.ok(); } } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/web/controller/StarQueryController.java ================================================ package com.acimage.community.web.controller; import com.acimage.common.deprecated.annotation.Authentication; import com.acimage.common.global.context.UserContext; import com.acimage.common.result.Result; import com.acimage.community.service.star.StarMixQueryService; import com.acimage.community.service.star.StarQueryService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import javax.validation.constraints.Positive; @RestController @Slf4j @Validated @Authentication @RequestMapping("/api/community/stars/query") public class StarQueryController { @Autowired StarQueryService starQueryService; @Autowired StarMixQueryService starMixQueryService; @GetMapping("/mine/{pageNo}") public Result pageMyStars(@Positive @PathVariable("pageNo") Integer pageNo) { return Result.ok(starMixQueryService.pageStarsWithTopic(UserContext.getUserId(),pageNo)); } @GetMapping("/isStar/{topicId}") public Result isStar(@Positive @PathVariable("topicId") Long topicId){ return Result.ok(starQueryService.isStar(UserContext.getUserId(),topicId)); } } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/web/controller/TagQueryController.java ================================================ package com.acimage.community.web.controller; import com.acimage.common.model.domain.community.Tag; import com.acimage.common.result.Result; import com.acimage.community.service.tag.TagQueryService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.List; @RestController @Slf4j @Validated @RequestMapping("/api/community/tags/query") public class TagQueryController { @Autowired TagQueryService tagQueryService; @GetMapping("/all") public Result> queryAllTags() { return Result.ok(tagQueryService.listAll()); } } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/web/controller/TopicOperateController.java ================================================ package com.acimage.community.web.controller; import com.acimage.common.deprecated.annotation.Authentication; import com.acimage.common.global.consts.FileFormatConstants; import com.acimage.common.global.consts.TimeConstants; import com.acimage.common.global.context.UserContext; import com.acimage.common.model.domain.community.Topic; import com.acimage.common.redis.annotation.RequestLimit; import com.acimage.common.redis.enums.LimitTarget; import com.acimage.common.result.Result; import com.acimage.common.utils.common.FileUtils; import com.acimage.community.model.request.TopicAddReq; import com.acimage.community.model.request.TopicModifyHtmlReq; import com.acimage.community.service.topic.TopicInfoWriteService; import com.acimage.community.service.topic.TopicWriteService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import javax.validation.constraints.Positive; import javax.validation.constraints.Size; @RestController @Slf4j @RequestMapping("/api/community/topics/operate") @Validated @Authentication public class TopicOperateController { @Autowired TopicInfoWriteService topicInfoWriteService; @Autowired TopicWriteService topicWriteService; @RequestLimit(limitTimes = {1, 10}, durations = {3, TimeConstants.DAY_SECONDS}, penaltyTimes = {-1, -1}, targets = {LimitTarget.IP, LimitTarget.USER}) @PostMapping public Result addTopic(@Validated @ModelAttribute TopicAddReq topicAddReq, @RequestParam("coverImage") MultipartFile coverImage) { String title = topicAddReq.getTitle().trim(); topicAddReq.setTitle(title); if (title.length() < Topic.TITLE_MIN || title.length() > Topic.TITLE_MAX) { return Result.fail(Topic.TAG_VALIDATION_MSG); } log.info("用户:{} 请求新增话题{}", UserContext.getUsername(), topicAddReq); String format = FileUtils.formatOf(coverImage); if (!FileFormatConstants.ALLOWED_COVER_IMAGE_FORMAT.contains(format)) { return Result.fail("封面图片格式需为" + FileFormatConstants.ALLOWED_IMAGE_FORMAT); } Integer[] tagIds = topicAddReq.getTagIds(); if (tagIds==null||tagIds.length > Topic.TAG_MAX || tagIds.length < Topic.TAG_MIN) { return Result.fail(Topic.TAG_VALIDATION_MSG); } log.info("用户:{} 话题: 新增话题{}", UserContext.getUsername(), topicAddReq); long topicId = topicInfoWriteService.saveTopicInfo(topicAddReq, coverImage); return Result.ok(topicId); } @RequestLimit(limitTimes = {1, 20}, durations = {3, TimeConstants.DAY_SECONDS}, penaltyTimes = {-1, -1}, targets = {LimitTarget.IP, LimitTarget.USER}) @PutMapping("/title/{id}/{title}") public Result modifyTitle(@Positive @PathVariable Long id, @PathVariable @Size(min = Topic.TITLE_MIN, max = Topic.TITLE_MAX, message = Topic.TITLE_VALIDATION_MSG) String title) { String trimTitle = title.trim(); if (trimTitle.length() < Topic.TITLE_MIN) { return Result.fail(String.format("标题有效长度不少于%s", Topic.TITLE_MIN)); } topicWriteService.updateTitle(id, trimTitle); return Result.ok(); } @RequestLimit(limitTimes = {1, 15}, durations = {3, TimeConstants.DAY_SECONDS}, penaltyTimes = {-1, -1}, targets = {LimitTarget.IP, LimitTarget.USER}) @PutMapping("/html") public Result modifyHtml(@Validated @RequestBody TopicModifyHtmlReq topicModifyHtmlReq) { topicInfoWriteService.updateHtml(topicModifyHtmlReq); return Result.ok(); } @RequestLimit(limitTimes = {1, 15}, durations = {3, TimeConstants.DAY_SECONDS}, penaltyTimes = {-1, -1}, targets = {LimitTarget.IP, LimitTarget.USER}) @DeleteMapping("/{id}") public Result deleteTopic(@Positive @PathVariable("id") Long id) { log.info("删除了话题:{}", id); topicInfoWriteService.removeTopicInfo(id); return Result.ok(); } } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/web/controller/TopicQueryController.java ================================================ package com.acimage.community.web.controller; import com.acimage.common.model.domain.community.Topic; import com.acimage.common.model.page.MyPage; import com.acimage.common.redis.annotation.RequestLimit; import com.acimage.common.redis.enums.LimitTarget; import com.acimage.common.result.Result; import com.acimage.common.deprecated.annotation.Authentication; import com.acimage.community.global.annotation.TopicId; import com.acimage.community.global.consts.PageSizeConstants; import com.acimage.community.global.enums.TopicAttribute; import com.acimage.community.model.request.TopicQueryByCategoryIdReq; import com.acimage.community.model.request.TopicQueryBySortReq; import com.acimage.community.model.request.TopicQueryByTagIdReq; import com.acimage.community.model.vo.TopicInfoVo; import com.acimage.community.service.topic.TopicEsSearchService; import com.acimage.community.service.topic.TopicInfoQueryService; import com.acimage.community.global.annotation.RecordPageView; import com.acimage.common.global.context.UserContext; import lombok.extern.slf4j.Slf4j; import org.hibernate.validator.constraints.Range; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import javax.validation.constraints.Positive; import java.util.ArrayList; import java.util.List; @RestController @Slf4j @RequestMapping("/api/community/topics/query") @Validated @Authentication public class TopicQueryController { @Autowired TopicInfoQueryService topicInfoQueryService; @Autowired TopicEsSearchService topicEsSearchService; @RequestLimit(limitTimes = {15}, durations = {5}, penaltyTimes = {-1}, targets = {LimitTarget.IP}) @RecordPageView @GetMapping("/info/{id}") public Result queryTopicAndFirstCommentPage(@TopicId @Positive @PathVariable("id") Long id) { TopicInfoVo topicInfoVo = topicInfoQueryService.getTopicInfoAndFirstCommentPage(id); return Result.ok(topicInfoVo); } @RequestLimit(limitTimes = {15}, durations = {5}, penaltyTimes = {-1}, targets = {LimitTarget.IP}) @GetMapping("/byTagId") public Result> pageByTagId(@Validated @ModelAttribute TopicQueryByTagIdReq queryReq) { return Result.ok(topicEsSearchService.searchByTagId(queryReq)); } @RequestLimit(limitTimes = {20}, durations = {5}, penaltyTimes = {-1}, targets = {LimitTarget.IP}) @GetMapping("/byCategoryId") public Result> pageByCategoryId(@Validated @ModelAttribute TopicQueryByCategoryIdReq queryReq) { return Result.ok(topicEsSearchService.searchBySort(queryReq)); } @RequestLimit(limitTimes = {15}, durations = {5}, penaltyTimes = {-1}, targets = {LimitTarget.IP}) @GetMapping("/bySort") public Result> pageTopicsBySort(@Validated @ModelAttribute TopicQueryBySortReq queryReq) { return Result.ok(topicEsSearchService.searchBySort(queryReq)); } /** * @return 三个列表,分别是最多评论,最多star,最多浏览的评论 */ @RequestLimit(limitTimes = {15}, durations = {5}, penaltyTimes = {-1}, targets = {LimitTarget.IP}) @GetMapping("/hot/3attrs") public Result>> combineHotTopics() { List> list=new ArrayList<>(); list.add(topicInfoQueryService.listTopicsInfoSortBy(TopicAttribute.COMMENT_COUNT,1,10)); list.add(topicInfoQueryService.listTopicsInfoSortBy(TopicAttribute.STAR_COUNT,1,10)); list.add(topicInfoQueryService.listTopicsInfoSortBy(TopicAttribute.PAGE_VIEW,1,10)); return Result.ok(list); } @RequestLimit(limitTimes = {15}, durations = {5}, penaltyTimes = {-1}, targets = {LimitTarget.IP}) @GetMapping("/recentHot") public Result> queryRecentHotTopics() { int pageNo = 1; int pageSize=10; return Result.ok(topicInfoQueryService.listTopicsInfoSortBy(TopicAttribute.PAGE_VIEW,pageNo,pageSize)); } @RequestLimit(limitTimes = {15}, durations = {5}, penaltyTimes = {-1}, targets = {LimitTarget.IP}) @GetMapping("/recommend") public Result> queryRecommendedTopics() { int size=4; return Result.ok(topicInfoQueryService.listRandomTopicsInRank(size)); } @RequestLimit(limitTimes = {15}, durations = {5}, penaltyTimes = {-1}, targets = {LimitTarget.IP}) @GetMapping("/most/commentCount") public Result> queryMostCommentCountTopics() { int pageNo = 1; int pageSize=10; return Result.ok(topicInfoQueryService.listTopicsInfoSortBy(TopicAttribute.COMMENT_COUNT,pageNo,pageSize)); } @RequestLimit(limitTimes = {15}, durations = {5}, penaltyTimes = {-1}, targets = {LimitTarget.IP}) @GetMapping("/most/active/{pageNo}/{pageSize}") public Result> pageActiveTopics(@Range(min = 1, max = 100, message = "页码在1到100之间") @PathVariable int pageNo, @Range(min = 4, max = 20, message = "页大小在4到20之间") @PathVariable int pageSize) { return Result.ok(topicInfoQueryService.pageTopicsInfoRankByActivityTime(pageNo, pageSize)); } @RequestLimit(limitTimes = {15}, durations = {5}, penaltyTimes = {-1}, targets = {LimitTarget.IP}) @GetMapping("/mine/{pageNo}") public Result> queryMyTopics(@Range(min = 1, max = 100, message = "页码在1到100之间") @PathVariable("pageNo") int pageNo) { return Result.ok(topicInfoQueryService.pageUserTopicsInfoOrderByCreateTime(UserContext.getUserId(), pageNo, PageSizeConstants.ACTIVITY_TOPICS)); } } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/web/controller/TopicSearchController.java ================================================ package com.acimage.community.web.controller; import com.acimage.common.model.domain.community.Topic; import com.acimage.common.model.page.MyPage; import com.acimage.common.redis.annotation.RequestLimit; import com.acimage.common.redis.enums.LimitTarget; import com.acimage.common.result.Result; import com.acimage.community.model.request.TopicSearchReq; import com.acimage.community.service.topic.TopicEsSearchService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; @RestController @Slf4j @RequestMapping("/api/community/topics/search") @Validated public class TopicSearchController { @Autowired TopicEsSearchService topicEsSearchService; @RequestLimit(limitTimes = {2}, durations = {1}, penaltyTimes = {-1}, targets = {LimitTarget.IP}) @GetMapping("/multiSearch") public Result> searchTopics(@Validated @ModelAttribute TopicSearchReq topicSearchReq) { String search= topicSearchReq.getSearch(); if(search!=null){ topicSearchReq.setSearch(search.trim()); } return Result.ok(topicEsSearchService.search(topicSearchReq)); } } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/web/controller/UserRankController.java ================================================ package com.acimage.community.web.controller; import com.acimage.common.model.domain.community.CmtyUser; import com.acimage.common.model.page.MyPage; import com.acimage.common.redis.annotation.RequestLimit; import com.acimage.common.redis.enums.LimitTarget; import com.acimage.common.result.Result; import com.acimage.common.utils.LambdaUtils; import com.acimage.community.service.cmtyuser.CmtyUserRankService; import lombok.extern.slf4j.Slf4j; import org.hibernate.validator.constraints.Range; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import javax.validation.constraints.Max; import javax.validation.constraints.Min; import java.util.List; @RestController @Slf4j @RequestMapping("/api/community/users/rank") @Validated public class UserRankController { @Autowired CmtyUserRankService cmtyUserRankService; @RequestLimit(limitTimes = {15}, durations = {5}, penaltyTimes = {-1}, targets = {LimitTarget.IP}) @GetMapping("/byColumn") public Result> rankByTopicCount(@RequestParam @Range(min=1,max=10) Integer pageNo, @RequestParam String column) { List allowedColumns= LambdaUtils.columnsFrom(CmtyUser::getTopicCount,CmtyUser::getStarCount); if(!allowedColumns.contains(column)){ return Result.fail("非法字段"); } int pageSize=10; return Result.ok(cmtyUserRankService.pageUserRankBy(column,pageNo, pageSize)); } } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/web/provider/CmtyUserProvider.java ================================================ package com.acimage.community.web.provider; import com.acimage.common.deprecated.annotation.Authentication; import com.acimage.common.model.domain.community.CmtyUser; import com.acimage.common.result.Result; import com.acimage.community.service.cmtyuser.CmtyUserQueryService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @Slf4j @RequestMapping("/community/cmtyUsers") @Validated public class CmtyUserProvider { @Autowired CmtyUserQueryService cmtyUserQueryService; @GetMapping("/userId/{userId}") public Result queryCmtyUser(@PathVariable Long userId) { return Result.ok(cmtyUserQueryService.getCmtyUser(userId)); } } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/web/provider/CommentProvider.java ================================================ package com.acimage.community.web.provider; import com.acimage.common.model.domain.community.Topic; import com.acimage.common.redis.annotation.RequestLimit; import com.acimage.common.redis.enums.LimitTarget; import com.acimage.common.result.Result; import com.acimage.community.service.comment.CommentWriteService; import com.acimage.community.service.topic.TopicInfoWriteService; import com.acimage.community.service.topic.TopicQueryService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import javax.validation.constraints.NotNull; import javax.validation.constraints.Positive; import java.util.List; @RestController @Slf4j @RequestMapping("/community/comments") @Validated public class CommentProvider { @Autowired CommentWriteService commentWriteService; @RequestLimit(limitTimes = {1},durations = {3},penaltyTimes = {1},targets = {LimitTarget.USER}) @DeleteMapping("/{id}") public Result deleteComment(@Positive @NotNull @PathVariable("id") Long id){ log.info("删除 评论{}",id); commentWriteService.removeCommentWithoutVerification(id); return Result.ok(); } } ================================================ FILE: acimage_community/src/main/java/com/acimage/community/web/provider/TopicProvider.java ================================================ package com.acimage.community.web.provider; import com.acimage.common.model.domain.community.Topic; import com.acimage.common.result.Result; import com.acimage.community.service.topic.TopicInfoWriteService; import com.acimage.community.service.topic.TopicQueryService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import javax.validation.constraints.Positive; import java.util.List; @RestController @Slf4j @RequestMapping("/community/topics") @Validated public class TopicProvider { @Autowired TopicQueryService topicQueryService; @Autowired TopicInfoWriteService topicInfoWriteService; @GetMapping("/ids") public Result> queryTopics(@RequestParam("topicIds") List topicIds) { return Result.ok(topicQueryService.listTopicWithUser(topicIds)); } @DeleteMapping("/{topicId}") public Result delete(@PathVariable @Positive Long topicId) { topicInfoWriteService.removeTopicInfoWithoutVerification(topicId); return Result.ok(); } } ================================================ FILE: acimage_community/src/main/resources/application-dev.yml ================================================ server: port: 8080 spring: config: activate: on-profile: - dev rabbitmq: virtual-host: /acimage host: 192.168.130.128 port: 5672 username: acimage password: acimage listener: simple: auto-startup: false #消费者是否自动启动 direct: auto-startup: false #生产者是否自动启动 datasource: url: jdbc:mysql://localhost:3306/acimage_community?useSSl=false&allowMultiQueries=true&serverTimezone=UTC username: root password: mysql redis: host: 192.168.130.128 port: 6379 password: redis lettuce: pool: max-active: 8 max-idle: 8 #最大空闲连接 min-idle: 0 #最小空闲连接 max-wait: 100ms #连接等待时间 cloud: nacos: server-addr: localhost:8848 #nacos 服务地址 ================================================ FILE: acimage_community/src/main/resources/application.yml ================================================ spring: profiles: include: common,common-secret active: dev2 servlet: multipart: max-file-size: 10MB max-request-size: 55MB application: name: community-service #服务名称 datasource: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.acimage.common.model.domain,com.acimage.community.model.domain configuration: map-underscore-to-camel-case: true # log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl global-config: db-config: table-prefix: tb_ feign: okhttp: enabled: true httpclient: max-connections: 20 # 最大的连接数 max-connections-per-route: 5 # 每个路径的最大连接数 ================================================ FILE: acimage_community/src/main/resources/logback-spring.xml ================================================ %d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%20.20thread{20}] %40logger{40} : %msg%n INFO ACCEPT DENY ${LOG_HOME}/${spring.application.name}/%d{yyyy-MM-dd}-info.%i.log 5MB 8 %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n WARN ACCEPT DENY ${LOG_HOME}/${spring.application.name}/%d{yyyy-MM-dd}-warn.%i.log 5MB 15 %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n ERROR ACCEPT DENY ${LOG_HOME}/${spring.application.name}/%d{yyyy-MM-dd}-error.%i.log 5MB 15 %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n ================================================ FILE: acimage_community/src/main/resources/mapper/CmtyUserMapper.xml ================================================ update tb_cmty_user set star_count = #{userIdAndIncrement.value} where id=#{userIdAndIncrement.key} update tb_cmty_user set topic_count = #{userIdAndIncrement.value} where id=#{userIdAndIncrement.key} ================================================ FILE: acimage_community/src/main/resources/mapper/CommentMapper.xml ================================================ ================================================ FILE: acimage_community/src/main/resources/mapper/ImageMapper.xml ================================================ insert into tb_image(id,topic_id,size,description,url) values ( #{image.id},#{image.topicId},#{image.size},#{image.description},#{image.url} ) update tb_image set description=#{idAndDescription.value} where id=#{idAndDescription.key} ================================================ FILE: acimage_community/src/main/resources/mapper/StarMapper.xml ================================================ ================================================ FILE: acimage_community/src/main/resources/mapper/TagTopicMapper.xml ================================================ insert into tb_tag_topic(id,topic_id,tag_id,create_time) values (#{item.id},#{item.topicId},#{item.tagId},#{item.createTime}) ================================================ FILE: acimage_community/src/main/resources/mapper/TopicMapper.xml ================================================ update tb_topic set page_view=page_view+#{idAndIncrement.value} where id=#{idAndIncrement.key} and deleted=0 update tb_topic set ${column} = ${column} + #{idAndIncrement.value} where id=#{idAndIncrement.key} and deleted=0 update tb_topic set activity_time = #{idAndActivityTime.value} where id=#{idAndActivityTime.key} and deleted=0 ================================================ FILE: acimage_community/src/test/java/com/acimage/community/CasualTest.java ================================================ package com.acimage.community; import cn.hutool.core.date.DateUtil; import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.crypto.digest.DigestUtil; import com.acimage.common.model.domain.image.Image; import com.acimage.common.utils.common.ListUtils; import com.acimage.community.global.enums.TopicAttribute; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import java.util.*; import java.util.regex.Pattern; @SpringBootTest public class CasualTest { @Test public void testGetSnowflakeId(){ long id=IdUtil.getSnowflake().nextId(); System.out.println(id); System.out.println(Long.toString(id).length()); System.out.println(System.currentTimeMillis()); TopicAttribute topicAttribute = TopicAttribute.ACTIVITY_TIME; } @Test public void testMd5(){ String txt="b3cfd02862e5565ebecfa608035911d612346546"; System.out.println(DigestUtil.md5Hex(txt).length()); } @Test public void testUUID(){ String uuid=IdUtil.simpleUUID(); System.out.println(uuid.length()); } @Test public void testPatternMatch(){ final String PASSWORD_PATTERN="^(?![a-zA-Z]+$)[0-9A-Za-z]{6,16}$"; String password="5555555----"; System.out.println(Pattern.matches(PASSWORD_PATTERN,password)); } @Test public void testDateNow(){ Date beginOfDay = DateUtil.beginOfDay(new Date()); //获取昨天开始时间 Date startDate=DateUtil.offsetDay(beginOfDay,-1); String startTime=DateUtil.format(startDate,"yyyy-MM-dd HH:mm:ss"); System.out.println(startTime); } @Test public void testSubAfter(){ String str="558.968.jpeg"; System.out.println(StrUtil.subAfter(str,'.',true)); List s= Arrays.asList("jpg","png"); System.out.println(s.contains("jpg")); } @Test public void testListUtils(){ List images=new ArrayList<>(); images.add(new Image(1L,null,null,null)); images.add(new Image(2L,null,null,null)); images.add(new Image(3L,null,null,null)); System.out.println(ListUtils.extract(Image::getId,images)); } } ================================================ FILE: acimage_community/src/test/java/com/acimage/community/CommunityApplicationTests.java ================================================ package com.acimage.community; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest class CommunityApplicationTests { @Test void contextLoads() { } } ================================================ FILE: acimage_community/src/test/java/com/acimage/community/dao/CommentDaoTest.java ================================================ package com.acimage.community.dao; import com.acimage.common.model.domain.community.Comment; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import java.util.List; @SpringBootTest public class CommentDaoTest { @Autowired CommentDao commentDao; @Test public void testSelectList(){ long topicId=1572508721685839872L; List commentList=commentDao.selectCommentsWithUser(topicId,1,5); for(Comment comment:commentList){ System.out.println(comment); } System.out.println(commentList.size()); } @Test public void testCountComments(){ long topicId=1572508721685839872L; System.out.println(commentDao.countCommentsByTopicId(topicId)); } @Test public void testSelectCommentsWithTopicOrderByCreateTime(){ long userId=1572443275490078720L; System.out.println(commentDao.selectCommentsWithTopicOrderByCreateTime(userId,0,10)); } } ================================================ FILE: acimage_community/src/test/java/com/acimage/community/dao/ImageDaoTest.java ================================================ package com.acimage.community.dao; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import java.util.Arrays; import java.util.List; @SpringBootTest public class ImageDaoTest { @Autowired ImageDao imageDao; @Test public void selectImagesWithTopic(){ List imageIds= Arrays.asList(1572508721903943680L,1572508721903943681L); System.out.println(imageDao.selectImagesWithTopic(imageIds)); } } ================================================ FILE: acimage_community/src/test/java/com/acimage/community/dao/StarDaoTest.java ================================================ package com.acimage.community.dao; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest public class StarDaoTest { @Autowired StarDao starDao; @Test public void countStarsByUserId(){ long userId=1572443275490078720L; System.out.println(starDao.countStarsOwnedBy(userId)); } @Test public void countSelectStarsWithTopic(){ long userId=1572443275490078720L; System.out.println(starDao.selectStarsWithTopicOrderByCreateTime(userId,1,5)); } } ================================================ FILE: acimage_community/src/test/java/com/acimage/community/dao/TopicDaoTest.java ================================================ package com.acimage.community.dao; import com.acimage.common.model.domain.community.Topic; import com.acimage.common.utils.LambdaUtils; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import java.util.List; @SpringBootTest public class TopicDaoTest { @Autowired TopicDao topicDao; @Test public void selectTopicWithUserImagesComments() { long id = 1572508721685839872L; } @Test public void selectTopicsOrderByScan() { String startTime = "2022-09-23 00:00:00"; List topics = topicDao.selectTopicsWithUserOrderByPageView(startTime, null); System.out.println(topics); } @Test public void getTopicCount() { long userId = 1572443275490078720L; } @Test public void testSelectTopicCount() { long userId = 0; System.out.println(topicDao.countTopics(userId)); } @Test public void testSelectTopicsWithUserOrderBy() { String column = LambdaUtils.underlineColumnNameOf(Topic::getPageView); List topicList = topicDao.selectTopicsWithUserOrderBy(column, 100); System.out.println(topicList); System.out.println(topicList.size()); } } ================================================ FILE: acimage_community/src/test/java/com/acimage/community/service/CommentWriteServiceTest.java ================================================ package com.acimage.community.service; import com.acimage.common.model.domain.community.Comment; import com.acimage.community.service.comment.CommentInfoQueryService; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import java.util.List; @SpringBootTest public class CommentWriteServiceTest { @Autowired CommentInfoQueryService commentIInfoService; } ================================================ FILE: acimage_community/src/test/java/com/acimage/community/service/TopicEsSearchServiceTest.java ================================================ package com.acimage.community.service; import com.acimage.community.service.topic.TopicEsSearchService; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest public class TopicEsSearchServiceTest { @Autowired TopicEsSearchService topicEsSearchService; } ================================================ FILE: acimage_community/src/test/java/com/acimage/community/utils/RedisTest.java ================================================ package com.acimage.community.utils; import com.acimage.common.model.domain.community.Topic; import com.acimage.common.utils.ExceptionUtils; import com.acimage.common.utils.redis.RedisUtils; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.acimage.community.dao.TopicDao; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.ZSetOperations; import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; @SpringBootTest public class RedisTest { @Autowired private StringRedisTemplate stringRedisTemplate; private static final ObjectMapper mapper=new ObjectMapper(); @Autowired TopicDao topicDao; @Autowired RedisUtils redisUtils; @Test void testStringRedisTemplate(){ stringRedisTemplate.opsForValue().set("test:Name","xxxyyy"); System.out.println(stringRedisTemplate.opsForValue().get("testName")); } @Test void testOpsForList(){ final String KEY="recentHotTopics"; String startTime="2022-09-23 00:00:00"; List topics = topicDao.selectTopicsWithUserOrderByPageView(startTime,null); System.out.println(stringRedisTemplate.opsForList()); } @Test void testOpsForValue(){ final String KEY="recentHotTopics"; String startTime="2022-09-23 00:00:00"; List topics = topicDao.selectTopicsWithUserOrderByPageView(startTime,null); String jsonTopics=null; try { jsonTopics=mapper.writeValueAsString(topics); System.out.println(jsonTopics); } catch (JsonProcessingException e) { throw new RuntimeException(e); } try { List topic2 =mapper.readValue(jsonTopics,new TypeReference>() { }); System.out.println(topic2); } catch (JsonProcessingException e) { throw new RuntimeException(e); } // stringRedisTemplate.opsForValue().set(KEY,topics.toString()) // System.out.println(stringRedisTemplate.opsForList()); } @Test void testRedisUtilsForList(){ final String KEY="recentHotTopics"; String startTime="2022-09-23 00:00:00"; List topics = topicDao.selectTopicsWithUserOrderByPageView(startTime,null); // try { // RedisUtils.setListAsString(stringRedisTemplate,KEY,topics,5, TimeUnit.SECONDS); // } catch (JsonProcessingException e) { // throw new RuntimeException(e); // } try { List topics1=redisUtils.getListFromString(KEY, Topic.class); System.out.println(topics1); } catch (Exception e) { ExceptionUtils.printIfDev(e); throw new RuntimeException(); } } @Test void testRedisUtilsForObject(){ final String KEY="topics"; String startTime="2022-09-23 00:00:00"; long id=1574286298175799296L; Topic topic = topicDao.selectById(id); redisUtils.setObjectJson(KEY, topic,50, TimeUnit.SECONDS); Topic topic1 =redisUtils.getObjectFromString(KEY, Topic.class); System.out.println(topic1); } @Test void testZSet(){ final String KEY="test-list"; //往ordered set设置值 stringRedisTemplate.opsForZSet().incrementScore(KEY,"value1",1); stringRedisTemplate.opsForZSet().incrementScore(KEY,"value2",5); stringRedisTemplate.opsForZSet().incrementScore(KEY,"value3",7); //查找 Set> set= stringRedisTemplate.opsForZSet().rangeWithScores(KEY,0L,9999999999L); Iterator> iterator = set.iterator(); while(iterator.hasNext()){ ZSetOperations.TypedTuple typedTuple=iterator.next(); System.out.println(typedTuple.getScore().longValue()); System.out.println(typedTuple.getValue()); } String[] t=new String[]{"value1"}; //删除 stringRedisTemplate.opsForZSet().remove(KEY,t); Double d=stringRedisTemplate.opsForZSet().score(KEY,"value4"); System.out.println(d); } @Test void testAddForZSet(){ final String KEY="test:zset"; stringRedisTemplate.opsForZSet().add(KEY,"v1",10); stringRedisTemplate.opsForZSet().add(KEY,"v1",20); } @Test void testRedisTransaction(){ stringRedisTemplate.opsForValue().set("X","Y"); stringRedisTemplate.setEnableTransactionSupport(true); stringRedisTemplate.multi(); stringRedisTemplate.exec(); stringRedisTemplate.setEnableTransactionSupport(false); System.out.println(redisUtils.delete("X")); } @Test void testHyperLogLog(){ String key="HyperLogLog"; long add=redisUtils.addForHyperLogLog(key,"v1","v2","v3"); Long size=redisUtils.sizeForHyperLogLog(key+"ddd"); System.out.println("add:"+add); System.out.println("size:"+size); redisUtils.deleteForHyperLogLog("hhhh"); } @Test void testSet(){ System.out.println(redisUtils.sizeForHyperLogLog("logKey")); } @Test void testExpire(){ String key="test:expire"; long timeout=100; stringRedisTemplate.opsForValue().set(key,"hahah"); boolean flag=redisUtils.expire(key,timeout,TimeUnit.SECONDS); System.out.println(flag); } @Test void testHash(){ stringRedisTemplate.opsForHash().increment("test:hash-key","star",10); System.out.println(stringRedisTemplate.opsForHash().get("test:hash-key","star")); } @Test void testLuaScriptIncrementIfPresent(){ String key="test:lua1"; redisUtils.setAsString(key,"0"); System.out.println(redisUtils.incrementIfPresent("test:lua1",10086L)); } } ================================================ FILE: acimage_feign/pom.xml ================================================ acimage com.acimage 0.0.1-SNAPSHOT 4.0.0 acimage_feign jar acimage_feign http://maven.apache.org UTF-8 com.acimage acimage_common 0.0.1-SNAPSHOT true org.springframework.cloud spring-cloud-starter-openfeign org.springframework.cloud spring-cloud-starter-loadbalancer io.github.openfeign feign-httpclient io.github.openfeign.form feign-form 3.8.0 io.github.openfeign.form feign-form-spring 3.8.0 javax.servlet javax.servlet-api provided org.springframework.boot spring-boot-maven-plugin exec true org.apache.maven.plugins maven-surefire-plugin true ================================================ FILE: acimage_feign/src/main/java/com/acimage/feign/FeignMain.java ================================================ package com.acimage.feign; /** * Hello world! * */ public class FeignMain { public static void main( String[] args ) { } } ================================================ FILE: acimage_feign/src/main/java/com/acimage/feign/client/CmtyUserClient.java ================================================ package com.acimage.feign.client; import com.acimage.common.model.domain.community.CmtyUser; import com.acimage.common.result.Result; import com.acimage.feign.fallback.CmtyUserClientFallbackFactory; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @FeignClient(value="community-service/community/cmtyUsers",fallbackFactory = CmtyUserClientFallbackFactory.class) public interface CmtyUserClient { @GetMapping("/userId/{userId}") Result queryCmtyUser(@PathVariable Long userId); } ================================================ FILE: acimage_feign/src/main/java/com/acimage/feign/client/CommentClient.java ================================================ package com.acimage.feign.client; import com.acimage.common.result.Result; import com.acimage.feign.fallback.CommentClientFallbackFactory; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.PathVariable; import java.util.List; @FeignClient(value="community-service/community/comments",fallbackFactory = CommentClientFallbackFactory.class) public interface CommentClient { @DeleteMapping("/{id}") public Result delete(@PathVariable("id") Long id) ; } ================================================ FILE: acimage_feign/src/main/java/com/acimage/feign/client/TopicClient.java ================================================ package com.acimage.feign.client; import com.acimage.common.model.domain.community.Topic; import com.acimage.common.result.Result; import com.acimage.feign.fallback.TopicClientFallbackFactory; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestParam; import java.util.List; @FeignClient(value="community-service/community/topics",fallbackFactory = TopicClientFallbackFactory.class) public interface TopicClient { @GetMapping("/ids") Result> queryTopics(@RequestParam("topicIds") List topicIds); @DeleteMapping("/{topicId}") public Result delete(@PathVariable("topicId") Long topicId) ; } ================================================ FILE: acimage_feign/src/main/java/com/acimage/feign/client/UserClient.java ================================================ package com.acimage.feign.client; import com.acimage.common.model.domain.user.User; import com.acimage.common.result.Result; import com.acimage.feign.fallback.UserClientFallbackFactory; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.*; @FeignClient(value="user-service/user/users",fallbackFactory = UserClientFallbackFactory.class) public interface UserClient { @GetMapping("/id/{id}") Result queryUser(@PathVariable Long id); @PutMapping("/photoUrl") Result modifyPhotoUrl(@RequestBody String photoUrl); } ================================================ FILE: acimage_feign/src/main/java/com/acimage/feign/config/FallbackFactoryBean.java ================================================ package com.acimage.feign.config; import com.acimage.feign.fallback.ImageClientFallbackFactory; import com.acimage.feign.fallback.UserClientFallbackFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; public class FallbackFactoryBean { @Bean public ImageClientFallbackFactory imageClientFallbackFactory(){ return new ImageClientFallbackFactory(); } @Bean public UserClientFallbackFactory userClientFallbackFactory(){ return new UserClientFallbackFactory(); } } ================================================ FILE: acimage_feign/src/main/java/com/acimage/feign/config/FeignMultipartSupportConfig.java ================================================ package com.acimage.feign.config; import feign.codec.Encoder; import feign.form.spring.SpringFormEncoder; import org.springframework.beans.factory.ObjectFactory; import org.springframework.boot.autoconfigure.http.HttpMessageConverters; import org.springframework.cloud.openfeign.support.SpringEncoder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class FeignMultipartSupportConfig { @Bean public Encoder feignFormEncoder(ObjectFactory messageConverters) { return new SpringFormEncoder(new SpringEncoder(messageConverters)); } } ================================================ FILE: acimage_feign/src/main/java/com/acimage/feign/config/FeignRequestInterceptorConfig.java ================================================ package com.acimage.feign.config; import com.acimage.common.global.consts.HeaderKeyConstants; import com.acimage.common.global.context.UserContext; import feign.RequestInterceptor; import feign.RequestTemplate; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Configuration; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest; import java.util.Enumeration; @Slf4j @Configuration public class FeignRequestInterceptorConfig implements RequestInterceptor { @Override public void apply(RequestTemplate requestTemplate) { ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); if(attrs==null){ log.error("feign请求为空"); return; } HttpServletRequest request = attrs.getRequest(); Enumeration attributeNames = request.getHeaderNames(); //设置header if (attributeNames != null) { while (attributeNames.hasMoreElements()) { String name = attributeNames.nextElement(); String value = request.getHeader(name); requestTemplate.header(name,value); // String KEY_COOKIE="cookie"; // if(KEY_COOKIE.equals(name)){ // requestTemplate.header(name,value); // return; // } } } //往header设置用户原始ip requestTemplate.header(HeaderKeyConstants.FEIGN_X_USER_IP, UserContext.getIp()); } } ================================================ FILE: acimage_feign/src/main/java/com/acimage/feign/depreted/FileClient.java ================================================ package com.acimage.feign.depreted; import com.acimage.common.result.Result; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import java.util.List; @FeignClient(value="image-service/api/image/hashImages") public interface FileClient { @GetMapping("sayHello") String sayHello(); @DeleteMapping Result deleteImageHashes(@RequestParam("imageIds") List imageIds); /** * 废弃原因:图片不再存储在本地,存储在七牛云 * @param imageFiles * @param imageIds * @return */ @Deprecated @PostMapping(value ="/imageFiles",produces = {MediaType.APPLICATION_JSON_VALUE},consumes = MediaType.MULTIPART_FORM_DATA_VALUE) Result storeImageFiles(@RequestPart("imageFiles") MultipartFile[] imageFiles, @RequestParam("imageIds") List imageIds); @PostMapping(value = "/uploadPhoto",produces = {MediaType.APPLICATION_JSON_VALUE},consumes = MediaType.MULTIPART_FORM_DATA_VALUE) Result storePhotoFiles(@RequestPart("photoFile") MultipartFile photoFile); } ================================================ FILE: acimage_feign/src/main/java/com/acimage/feign/depreted/ImageClient.java ================================================ package com.acimage.feign.depreted; import com.acimage.common.model.domain.image.Image; import com.acimage.feign.fallback.ImageClientFallbackFactory; import com.acimage.common.result.Result; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestParam; import java.util.List; @FeignClient(value="image-service/image/images",fallbackFactory = ImageClientFallbackFactory.class) public interface ImageClient { @GetMapping("/imageIds") Result queryImagesWithTopic(@RequestParam("imageIds") List imageIds); @GetMapping("/topicId/{topicId}") Result> queryTopicImages(@PathVariable("topicId") Long topicId) ; @GetMapping("/preparedTopicImages") Result updateTopicIdAndReturnFirstImageUrl(@RequestParam("serviceToken") String serviceToken, @RequestParam("topicId") Long topicId); } ================================================ FILE: acimage_feign/src/main/java/com/acimage/feign/fallback/CmtyUserClientFallbackFactory.java ================================================ package com.acimage.feign.fallback; import com.acimage.common.model.domain.community.CmtyUser; import com.acimage.common.result.Result; import com.acimage.common.utils.ExceptionUtils; import com.acimage.feign.client.CmtyUserClient; import lombok.extern.slf4j.Slf4j; import org.springframework.cloud.openfeign.FallbackFactory; import org.springframework.stereotype.Component; @Component @Slf4j public class CmtyUserClientFallbackFactory implements FallbackFactory { @Override public CmtyUserClient create(Throwable cause) { return new CmtyUserClient() { @Override public Result queryCmtyUser(Long userId) { ExceptionUtils.printIfDev(cause); log.error("查询UserCommunityStatistic失败,userId:{}", userId); return Result.ok(new CmtyUser()); } }; } } ================================================ FILE: acimage_feign/src/main/java/com/acimage/feign/fallback/CommentClientFallbackFactory.java ================================================ package com.acimage.feign.fallback; import com.acimage.common.model.domain.community.Topic; import com.acimage.common.result.Result; import com.acimage.common.utils.ExceptionUtils; import com.acimage.feign.client.CommentClient; import com.acimage.feign.client.TopicClient; import lombok.extern.slf4j.Slf4j; import org.springframework.cloud.openfeign.FallbackFactory; import org.springframework.stereotype.Component; import java.util.ArrayList; import java.util.List; @Slf4j @Component public class CommentClientFallbackFactory implements FallbackFactory { @Override public CommentClient create(Throwable cause) { return new CommentClient() { @Override public Result delete(Long id) { log.error("删除评论失败 id:{} error:{}", id, cause.getMessage()); return Result.fail("删除评论失败"); } }; } } ================================================ FILE: acimage_feign/src/main/java/com/acimage/feign/fallback/ImageClientFallbackFactory.java ================================================ package com.acimage.feign.fallback; import com.acimage.common.model.domain.image.Image; import com.acimage.common.utils.ExceptionUtils; import com.acimage.common.utils.SpringContextUtils; import com.acimage.feign.depreted.ImageClient; import com.acimage.common.result.Result; import lombok.extern.slf4j.Slf4j; import org.springframework.cloud.openfeign.FallbackFactory; import org.springframework.stereotype.Component; import java.util.ArrayList; import java.util.List; @Component @Slf4j public class ImageClientFallbackFactory implements FallbackFactory { @Override public ImageClient create(Throwable cause) { return new ImageClient() { @Override public Result queryImagesWithTopic(List imageIds) { if(SpringContextUtils.isDev()){ ExceptionUtils.printIfDev(cause); } log.error("feign 查询失败,imageIds:{}",imageIds); return Result.fail("服务繁忙,查询失败"); } @Override public Result> queryTopicImages(Long topicId) { ExceptionUtils.printIfDev(cause); log.error("feign 查询话题图片失败,topicId:{}",topicId); return Result.ok(new ArrayList<>()); } @Override public Result updateTopicIdAndReturnFirstImageUrl(String serviceToken, Long topicId) { ExceptionUtils.printIfDev(cause); log.error("feign topicId提交到image-service失败,topicId:{}",topicId); return Result.fail("服务繁忙,查询失败"); } }; } } ================================================ FILE: acimage_feign/src/main/java/com/acimage/feign/fallback/TopicClientFallbackFactory.java ================================================ package com.acimage.feign.fallback; import com.acimage.common.model.domain.community.Topic; import com.acimage.common.result.Result; import com.acimage.common.utils.ExceptionUtils; import com.acimage.feign.client.TopicClient; import lombok.extern.slf4j.Slf4j; import org.springframework.cloud.openfeign.FallbackFactory; import org.springframework.stereotype.Component; import java.util.ArrayList; import java.util.List; @Slf4j @Component public class TopicClientFallbackFactory implements FallbackFactory { @Override public TopicClient create(Throwable cause) { return new TopicClient() { @Override public Result> queryTopics(List topicIds) { ExceptionUtils.printIfDev(cause); log.error("查询多个话题失败 topicIds:{} error:{}", topicIds,cause.getMessage()); return Result.ok(new ArrayList<>()); } @Override public Result delete(Long topicId) { log.error("删除话题失败 id:{} error:{}",topicId,cause.getMessage()); return Result.fail("删除话题失败"); } }; } } ================================================ FILE: acimage_feign/src/main/java/com/acimage/feign/fallback/UserClientFallbackFactory.java ================================================ package com.acimage.feign.fallback; import com.acimage.common.global.context.UserContext; import com.acimage.common.model.domain.user.User; import com.acimage.common.result.Result; import com.acimage.common.utils.ExceptionUtils; import com.acimage.feign.client.UserClient; import lombok.extern.slf4j.Slf4j; import org.springframework.cloud.openfeign.FallbackFactory; import org.springframework.stereotype.Component; @Component @Slf4j public class UserClientFallbackFactory implements FallbackFactory { @Override public UserClient create(Throwable cause) { return new UserClient() { @Override public Result queryUser(Long id) { ExceptionUtils.printIfDev(cause); log.error("查询失败,用户id:{}", id); return Result.ok(new User()); } @Override public Result modifyPhotoUrl(String photoUrl) { ExceptionUtils.printIfDev(cause); log.error("修改photoUrl失败,用户id:{}", UserContext.getUsername()); return Result.fail("头像修改失败"); } }; } } ================================================ FILE: acimage_gateway/pom.xml ================================================ acimage com.acimage 0.0.1-SNAPSHOT 4.0.0 acimage_gateway jar acimage_gateway http://maven.apache.org UTF-8 com.acimage acimage_common 0.0.1-SNAPSHOT org.springframework.boot spring-boot-starter-web com.alibaba druid mysql mysql-connector-java runtime org.springframework.cloud spring-cloud-starter-gateway org.springframework.cloud spring-cloud-starter-openfeign org.springframework.cloud spring-cloud-loadbalancer com.github.ben-manes.caffeine caffeine org.apache.maven.plugins maven-surefire-plugin true ================================================ FILE: acimage_gateway/src/main/java/com/acimage/gateway/GatewayApplication.java ================================================ package com.acimage.gateway; import lombok.extern.slf4j.Slf4j; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.ComponentScans; import org.springframework.stereotype.Component; @Slf4j @EnableDiscoveryClient @SpringBootApplication @MapperScan("com.acimage.gateway.dao") @ComponentScan(value = {"com.acimage.gateway","com.acimage.common.utils"}) public class GatewayApplication { public static void main(String[] args) { SpringApplication.run(GatewayApplication.class, args); log.info("------------->>>Gateway启动<<<-------------"); } } ================================================ FILE: acimage_gateway/src/main/java/com/acimage/gateway/apitree/ApiTree.java ================================================ package com.acimage.gateway.apitree; import com.acimage.common.global.enums.MyHttpMethod; import com.acimage.common.model.domain.sys.Api; import lombok.Data; import lombok.NoArgsConstructor; import org.springframework.http.HttpMethod; import java.util.ArrayList; import java.util.List; import java.util.concurrent.ConcurrentMap; @NoArgsConstructor @Data public class ApiTree { /** * 路径id // */ // private List ids=new ArrayList<>(); // /** // * 权限id // */ // private List permissionIds=new ArrayList<>(); // private List methods=new ArrayList<>(); private List apiList=new ArrayList<>(); private ConcurrentMap children; } ================================================ FILE: acimage_gateway/src/main/java/com/acimage/gateway/apitree/ApiTreeFactory.java ================================================ package com.acimage.gateway.apitree; import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; @Component @Slf4j @Data public class ApiTreeFactory { private ApiTree prefixMatchApiTree; private ApiTree exactMatchApiTree; private ApiTree apiTree; } ================================================ FILE: acimage_gateway/src/main/java/com/acimage/gateway/apitree/ApiTreeUtils.java ================================================ package com.acimage.gateway.apitree; import cn.hutool.core.collection.CollectionUtil; import com.acimage.common.global.enums.MyHttpMethod; import com.acimage.common.model.domain.sys.Api; import com.acimage.gateway.global.consts.NotationConstants; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpMethod; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @Slf4j public class ApiTreeUtils { public static Api getMatchApi(ApiTree apiTree, String path, HttpMethod method) { if (apiTree == null) { return null; } if (path.startsWith("/")) { if (path.length() == 1) { return null; } path = path.substring(1); } List splits = Arrays.asList(path.split("/")); if (CollectionUtil.isEmpty(splits)) { return null; } return recursiveMatch(apiTree, splits, 0, method); } private static Api recursiveMatch(ApiTree apiTree, List splits, int beginIndex, HttpMethod method) { if (beginIndex == splits.size() || apiTree == null||apiTree.getChildren()==null) { return null; } for (int i = beginIndex; i < splits.size(); i++) { String s = splits.get(i); ConcurrentMap children = apiTree.getChildren(); //待匹配字符串 List matchStrings = Arrays.asList(s, NotationConstants.ASTERISK, NotationConstants.DOUBLE_ASTERISK); for (String matchString : matchStrings) { ApiTree child = children.get(matchString); //刚好匹配到根节点或匹配到** if (child != null && (i == splits.size() - 1 || matchString.equals(NotationConstants.DOUBLE_ASTERISK))) { int matchIndex = -1; List apiList = child.getApiList(); //找到匹配的api for (int j = 0; j < apiList.size(); j++) { if (apiList.get(j).getMethod().equals(MyHttpMethod.from(method))) { matchIndex = j; break; } else if (apiList.get(j).getMethod().equals(MyHttpMethod.ALL)) { matchIndex = j; } } //匹配到则返回 if (matchIndex != -1) { return apiList.get(matchIndex); } //再看看下个结点有没有**的子节点 if (i == splits.size() - 1 && child.getChildren() != null) { for (String str : Arrays.asList(NotationConstants.ASTERISK, NotationConstants.DOUBLE_ASTERISK)) { ApiTree tempChild=child.getChildren().get(str); if(tempChild==null){ continue; } matchIndex = -1; apiList =tempChild.getApiList(); //找到匹配的api for (int j = 0; j < apiList.size(); j++) { if (apiList.get(j).getMethod().equals(MyHttpMethod.from(method))) { matchIndex = j; break; } else if (apiList.get(j).getMethod().equals(MyHttpMethod.ALL)) { matchIndex = j; } } //匹配到则返回 if (matchIndex != -1) { return apiList.get(matchIndex); } } } } else if (child != null) { //递归匹配 Api result = recursiveMatch(child, splits, i + 1, method); if (result != null) { return result; } } } } return null; } public static ApiTree buildApiTreeFrom(List apiList) { //按/分割路径 //统一去掉斜杆开头 for (Api api : apiList) { String path = api.getPath(); if (path.startsWith("/")) { api.setPath(path.substring(1)); } } apiList.sort(ApiTreeUtils::comparator); List> splits = new ArrayList<>(); for (Api api : apiList) { splits.add(Arrays.asList(api.getPath().split("/"))); } ApiTree apiTree = new ApiTree(); for (int i = 0; i < splits.size(); i++) { ApiTree head = apiTree; List pathSplits = splits.get(i); for (int j = 0; j < pathSplits.size(); j++) { if (head.getChildren() == null) { head.setChildren(new ConcurrentHashMap<>()); } ConcurrentMap children = head.getChildren(); ApiTree child = children.get(pathSplits.get(j)); if (child == null) { children.put(pathSplits.get(j), new ApiTree()); } child = children.get(pathSplits.get(j)); if (j == pathSplits.size() - 1) { child.getApiList().add(apiList.get(i)); } head = child; } } return apiTree; } private static int comparator(Api next, Api cur) { List nextList = Arrays.asList(next.getPath().split("/")); List curList = Arrays.asList(cur.getPath().split("/")); for (int i = 0; i < nextList.size() && i < curList.size(); i++) { String sNext = nextList.get(i); String sCur = curList.get(i); if (sNext.compareTo(sCur) == 0) { continue; } else if (orderOf(sNext) != orderOf(sCur)) { return orderOf(sNext) - orderOf(sCur); } { return sNext.compareTo(sCur); } } return curList.size() - nextList.size(); } private static int orderOf(String s) { if (NotationConstants.DOUBLE_ASTERISK.equals(s)) { return 2; } if (NotationConstants.ASTERISK.equals(s)) { return 1; } return 0; } } ================================================ FILE: acimage_gateway/src/main/java/com/acimage/gateway/apitree/InitApiTreeApplicationRunner.java ================================================ package com.acimage.gateway.apitree; import com.acimage.common.utils.redis.RedisUtils; import com.acimage.gateway.serivce.ApiQueryService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Component; @Slf4j @Component public class InitApiTreeApplicationRunner implements ApplicationRunner { @Autowired ApiQueryService apiQueryService; @Autowired ApiTreeFactory apiTreeFactory; @Override public void run(ApplicationArguments args) { log.info("开始初始化apiTree"); ApiTree apiTree=ApiTreeUtils.buildApiTreeFrom(apiQueryService.listEnableApis()); apiTreeFactory.setApiTree(apiTree); log.info("初始化api tree完成"); } } ================================================ FILE: acimage_gateway/src/main/java/com/acimage/gateway/config/KeySolverConfig.java ================================================ package com.acimage.gateway.config; import com.acimage.common.utils.IpUtils; import lombok.extern.slf4j.Slf4j; import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.server.reactive.ServerHttpRequest; import reactor.core.publisher.Mono; @Slf4j @Configuration public class KeySolverConfig { @Bean public KeyResolver ipKeyResolver() { return exchange ->{ ServerHttpRequest request=exchange.getRequest(); return Mono.just(IpUtils.getUserIp(request)); }; } } ================================================ FILE: acimage_gateway/src/main/java/com/acimage/gateway/config/RoleConfig.java ================================================ package com.acimage.gateway.config; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Configuration; @Configuration @Data @ConfigurationProperties(prefix = "role") public class RoleConfig { private int visitorId; private int userId; } ================================================ FILE: acimage_gateway/src/main/java/com/acimage/gateway/dao/ApiDao.java ================================================ package com.acimage.gateway.dao; import com.acimage.common.model.domain.sys.Api; import com.baomidou.mybatisplus.core.mapper.BaseMapper; public interface ApiDao extends BaseMapper { } ================================================ FILE: acimage_gateway/src/main/java/com/acimage/gateway/dao/AuthorizeDao.java ================================================ package com.acimage.gateway.dao; import com.acimage.common.model.domain.sys.Authorize; import com.baomidou.mybatisplus.core.mapper.BaseMapper; public interface AuthorizeDao extends BaseMapper { } ================================================ FILE: acimage_gateway/src/main/java/com/acimage/gateway/dao/PermissionDao.java ================================================ package com.acimage.gateway.dao; import com.acimage.common.model.domain.sys.Permission; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Param; import javax.annotation.Nullable; import java.util.List; public interface PermissionDao extends BaseMapper { List selectTreeByParentId(@Nullable @Param("parentId") Integer parentId); List selectPermissionsWithParent(@Param("startIndex") int startIndex,@Param("recordNumber") int recordNumber); } ================================================ FILE: acimage_gateway/src/main/java/com/acimage/gateway/dao/RoleDao.java ================================================ package com.acimage.gateway.dao; import com.acimage.common.model.domain.sys.Role; import com.baomidou.mybatisplus.core.mapper.BaseMapper; public interface RoleDao extends BaseMapper { } ================================================ FILE: acimage_gateway/src/main/java/com/acimage/gateway/dao/UserRoleDao.java ================================================ package com.acimage.gateway.dao; import com.acimage.common.model.domain.sys.UserRole; import com.baomidou.mybatisplus.core.mapper.BaseMapper; public interface UserRoleDao extends BaseMapper { } ================================================ FILE: acimage_gateway/src/main/java/com/acimage/gateway/global/consts/NotationConstants.java ================================================ package com.acimage.gateway.global.consts; public class NotationConstants { public static final String ASTERISK="*"; public static final String DOUBLE_ASTERISK="**"; } ================================================ FILE: acimage_gateway/src/main/java/com/acimage/gateway/globalfilter/AuthenticationFilter.java ================================================ package com.acimage.gateway.globalfilter; import com.acimage.common.global.consts.SysKeyConstants; import com.acimage.common.global.exception.NullTokenException; import com.acimage.common.global.consts.HeaderKeyConstants; import com.acimage.common.global.context.UserContext; import com.acimage.common.service.impl.TokenServiceImpl; import com.acimage.common.utils.IpUtils; import com.acimage.common.utils.JwtUtils; import com.acimage.common.utils.redis.RedisUtils; import com.auth0.jwt.exceptions.JWTVerificationException; import com.auth0.jwt.exceptions.TokenExpiredException; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.core.annotation.Order; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; @Slf4j @Order(20) @Component public class AuthenticationFilter implements GlobalFilter { @Autowired RedisUtils redisUtils; @Override public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request = exchange.getRequest(); String url = request.getURI().getPath(); String token = request.getHeaders().getFirst(HeaderKeyConstants.AUTHORIZATION); String ip = IpUtils.getUserIp(request); UserContext.setIp(ip); //记录接口访问次数 redisUtils.increment(SysKeyConstants.STRINGK_INTERFACE_TOTAL,1); //记录访问量 redisUtils.addForHyperLogLog(SysKeyConstants.LOGK_PAGE_VIEW,ip); boolean isException = false; //验证token try { JwtUtils.verifyToken(token); } catch (NullTokenException e) { log.debug("access 无token 访问:{} ip:{}", url, ip); isException = true; } catch (TokenExpiredException e) { log.info("access token过期 用户:{} 访问:{} ip:{}", JwtUtils.getUsername(token), url, ip); isException = true; } catch (JWTVerificationException e1) { log.warn("access 非法token 访问:{} ip:{} token:{}", url, ip,token); isException = true; } if (!isException) { if (!this.hasRecorded(token)) { log.info("access token未被记录 用户:{} 访问:{} ip:{}", JwtUtils.getUsername(token), url, ip); return chain.filter(exchange); } String method = request.getMethodValue(); log.debug("access 用户:{} 访问:{} {} ip:{}", JwtUtils.getUsername(token), url, method, ip); UserContext.setUserId(JwtUtils.getUserId(token)); UserContext.setUsername(JwtUtils.getUsername(token)); UserContext.setIp(ip); } return chain.filter(exchange); } private boolean hasRecorded(String token) { return redisUtils.getForString(TokenServiceImpl.STRINGKP_TOKEN+token)!=null; } } ================================================ FILE: acimage_gateway/src/main/java/com/acimage/gateway/globalfilter/CustomWebsocketRoutingFilter.java ================================================ package com.acimage.gateway.globalfilter; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.ObjectProvider; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.cloud.gateway.filter.headers.HttpHeadersFilter; import org.springframework.cloud.gateway.support.ServerWebExchangeUtils; import org.springframework.core.Ordered; import org.springframework.http.HttpHeaders; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import org.springframework.web.reactive.socket.WebSocketHandler; import org.springframework.web.reactive.socket.WebSocketMessage; import org.springframework.web.reactive.socket.WebSocketSession; import org.springframework.web.reactive.socket.client.WebSocketClient; import org.springframework.web.reactive.socket.server.WebSocketService; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.util.UriComponentsBuilder; import reactor.core.publisher.Mono; import java.net.URI; import java.util.*; /** * 解决websocket关闭异常 问题 * @author admin * @Desc websocket客户端主动断开连接,网关服务报错1005 * @date 2022/8/24 14:30 */ @Component public class CustomWebsocketRoutingFilter implements GlobalFilter, Ordered { public static final String SEC_WEBSOCKET_PROTOCOL = "Sec-WebSocket-Protocol"; private static final Log log = LogFactory.getLog(CustomWebsocketRoutingFilter.class); private final WebSocketClient webSocketClient; private final WebSocketService webSocketService; private final ObjectProvider> headersFiltersProvider; private volatile List headersFilters; public CustomWebsocketRoutingFilter(WebSocketClient webSocketClient, WebSocketService webSocketService, ObjectProvider> headersFiltersProvider) { this.webSocketClient = webSocketClient; this.webSocketService = webSocketService; this.headersFiltersProvider = headersFiltersProvider; } static String convertHttpToWs(String scheme) { scheme = scheme.toLowerCase(); return "http".equals(scheme) ? "ws" : ("https".equals(scheme) ? "wss" : scheme); } @Override public int getOrder() { return 2147483645; } @Override public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { changeSchemeIfIsWebSocketUpgrade(exchange); URI requestUrl = (URI)exchange.getRequiredAttribute(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR); //log.debug(requestUrl); String scheme = requestUrl.getScheme(); if (!ServerWebExchangeUtils.isAlreadyRouted(exchange) && ("ws".equals(scheme) || "wss".equals(scheme))) { ServerWebExchangeUtils.setAlreadyRouted(exchange); HttpHeaders headers = exchange.getRequest().getHeaders(); HttpHeaders filtered = HttpHeadersFilter.filterRequest(this.getHeadersFilters(), exchange); List protocols = this.getProtocols(headers); return this.webSocketService.handleRequest(exchange, new CustomWebsocketRoutingFilter.ProxyWebSocketHandler(requestUrl, this.webSocketClient, filtered, protocols)); } else { return chain.filter(exchange); } } List getProtocols(HttpHeaders headers) { List protocols = headers.get("Sec-WebSocket-Protocol"); if (protocols != null) { ArrayList updatedProtocols = new ArrayList(); for(int i = 0; i < ((List)protocols).size(); ++i) { String protocol = (String)((List)protocols).get(i); updatedProtocols.addAll(Arrays.asList(StringUtils.tokenizeToStringArray(protocol, ","))); } protocols = updatedProtocols; } return (List)protocols; } List getHeadersFilters() { if (this.headersFilters == null) { this.headersFilters = (List)this.headersFiltersProvider.getIfAvailable(ArrayList::new); this.headersFilters.add((headers, exchange) -> { HttpHeaders filtered = new HttpHeaders(); filtered.addAll(headers); filtered.remove("Host"); boolean preserveHost = (Boolean)exchange.getAttributeOrDefault(ServerWebExchangeUtils.PRESERVE_HOST_HEADER_ATTRIBUTE, false); if (preserveHost) { String host = exchange.getRequest().getHeaders().getFirst("Host"); filtered.add("Host", host); } return filtered; }); this.headersFilters.add((headers, exchange) -> { HttpHeaders filtered = new HttpHeaders(); Iterator var3 = headers.entrySet().iterator(); while(var3.hasNext()) { Map.Entry> entry = (Map.Entry)var3.next(); if (!((String)entry.getKey()).toLowerCase().startsWith("sec-websocket")) { filtered.addAll((String)entry.getKey(), (List)entry.getValue()); } } return filtered; }); } return this.headersFilters; } static void changeSchemeIfIsWebSocketUpgrade(ServerWebExchange exchange) { URI requestUrl = (URI)exchange.getRequiredAttribute(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR); String scheme = requestUrl.getScheme().toLowerCase(); String upgrade = exchange.getRequest().getHeaders().getUpgrade(); if ("WebSocket".equalsIgnoreCase(upgrade) && ("http".equals(scheme) || "https".equals(scheme))) { String wsScheme = convertHttpToWs(scheme); boolean encoded = ServerWebExchangeUtils.containsEncodedParts(requestUrl); URI wsRequestUrl = UriComponentsBuilder.fromUri(requestUrl).scheme(wsScheme).build(encoded).toUri(); exchange.getAttributes().put(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR, wsRequestUrl); if (log.isTraceEnabled()) { log.trace("changeSchemeTo:[" + wsRequestUrl + "]"); } } } private static class ProxyWebSocketHandler implements WebSocketHandler { private final WebSocketClient client; private final URI url; private final HttpHeaders headers; private final List subProtocols; ProxyWebSocketHandler(URI url, WebSocketClient client, HttpHeaders headers, List protocols) { this.client = client; this.url = url; this.headers = headers; if (protocols != null) { this.subProtocols = protocols; } else { this.subProtocols = Collections.emptyList(); } } @Override public List getSubProtocols() { return this.subProtocols; } @Override public Mono handle(WebSocketSession session) { return this.client.execute(this.url, this.headers, new WebSocketHandler() { @Override public Mono handle(WebSocketSession proxySession) { Mono serverClose = proxySession.closeStatus().filter(__ -> session.isOpen()) .flatMap(session::close); Mono proxyClose = session.closeStatus().filter(__ -> proxySession.isOpen()) .flatMap(proxySession::close); // Use retain() for Reactor Netty Mono proxySessionSend = proxySession .send(session.receive().doOnNext(WebSocketMessage::retain)); Mono serverSessionSend = session .send(proxySession.receive().doOnNext(WebSocketMessage::retain)); return Mono.zip(proxySessionSend, serverSessionSend, serverClose, proxyClose).then(); } @Override public List getSubProtocols() { return CustomWebsocketRoutingFilter.ProxyWebSocketHandler.this.subProtocols; } }); } } } ================================================ FILE: acimage_gateway/src/main/java/com/acimage/gateway/globalfilter/PermissionFilter.java ================================================ package com.acimage.gateway.globalfilter; import com.acimage.common.global.context.UserContext; import com.acimage.common.model.domain.sys.Api; import com.acimage.gateway.apitree.ApiTreeFactory; import com.acimage.gateway.apitree.ApiTreeUtils; import com.acimage.gateway.config.RoleConfig; import com.acimage.gateway.serivce.AuthorizeQueryService; import com.acimage.gateway.serivce.RoleQueryService; import com.acimage.gateway.serivce.UserRoleQueryService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.core.annotation.Order; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; import java.util.List; import java.util.Map; @Slf4j @Order(30) @Component public class PermissionFilter implements GlobalFilter { @Autowired private ApiTreeFactory apiTreeFactory; @Autowired private AuthorizeQueryService authorizeQueryService; @Autowired private UserRoleQueryService userRoleQueryService; @Autowired private RoleConfig roleConfig; @Override public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request = exchange.getRequest(); String url = request.getURI().getPath(); HttpMethod httpMethod = request.getMethod(); if (httpMethod != HttpMethod.GET) { log.info("{} {} ip:{} user:{}", url, httpMethod, UserContext.getIp(), UserContext.getUsername()); } //获取匹配的api树 Api api = ApiTreeUtils.getMatchApi(apiTreeFactory.getApiTree(), url, httpMethod); //api不存在 if (api == null) { log.info(url + " 不存在"); UserContext.remove(); return exchange.getResponse().setComplete(); } Map> map = authorizeQueryService.getRolePermissionIdsMap(); //获取访客权限 List permissionIds = map.get(roleConfig.getVisitorId()); if (permissionIds != null && permissionIds.contains(api.getPermissionId())) { log.debug(api.getPath() + api.getMethod() + "通过"); UserContext.remove(); return chain.filter(exchange); } //获取用户权限 if (UserContext.getUserId() != null) { permissionIds = map.get(roleConfig.getUserId()); if (permissionIds != null && permissionIds.contains(api.getPermissionId())) { log.debug(api.getPath() + api.getMethod() + "通过"); UserContext.remove(); return chain.filter(exchange); } } if (UserContext.getUserId() != null) { //获取用户具体角色的权限 List roleIds = userRoleQueryService.listRoleIds(UserContext.getUserId()); for (Integer roleId : roleIds) { permissionIds = map.get(roleId); if (permissionIds != null && permissionIds.contains(api.getPermissionId())) { log.debug(api.getPath() + api.getMethod() + "通过"); UserContext.remove(); return chain.filter(exchange); } } } log.info("{} {} 权限不足 ip:{}", api.getPath(), api.getMethod(), UserContext.getIp()); UserContext.remove(); exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); } } ================================================ FILE: acimage_gateway/src/main/java/com/acimage/gateway/globalfilter/RemoveContextFilter.java ================================================ package com.acimage.gateway.globalfilter; import com.acimage.common.global.context.UserContext; import lombok.extern.slf4j.Slf4j; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.core.annotation.Order; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; @Slf4j @Order(Integer.MAX_VALUE) public class RemoveContextFilter implements GlobalFilter { @Override public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { UserContext.remove(); return chain.filter(exchange); } } ================================================ FILE: acimage_gateway/src/main/java/com/acimage/gateway/globalfilter/RequestLimitFilter.java ================================================ package com.acimage.gateway.globalfilter; import com.acimage.common.utils.redis.RedisUtils; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.cloud.gateway.support.ServerWebExchangeUtils; import org.springframework.core.annotation.Order; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; import java.net.URI; import java.util.concurrent.TimeUnit; @Slf4j @Order(5) @Component public class RequestLimitFilter implements GlobalFilter { @Autowired private RedisUtils redisUtils; public static final int TOTAL_LIMIT = 1000; public static final String STRINGK_TOTAL_LIMIT = "acimage:gateway:limit:total"; @Override public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { int increment = 1; long timeoutSeconds = 1L; Long count = redisUtils.increment(STRINGK_TOTAL_LIMIT, increment); if (count == 1) { redisUtils.expire(STRINGK_TOTAL_LIMIT, timeoutSeconds, TimeUnit.SECONDS); } else if (count >= TOTAL_LIMIT) { exchange.getResponse().setStatusCode(HttpStatus.TOO_MANY_REQUESTS); return exchange.getResponse().setComplete(); } return chain.filter(exchange); } } ================================================ FILE: acimage_gateway/src/main/java/com/acimage/gateway/schedule/RefreshApiTreeSchedule.java ================================================ package com.acimage.gateway.schedule; import com.acimage.gateway.apitree.ApiTree; import com.acimage.gateway.apitree.ApiTreeFactory; import com.acimage.gateway.apitree.ApiTreeUtils; import com.acimage.gateway.serivce.ApiQueryService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; @Slf4j @Component @EnableScheduling public class RefreshApiTreeSchedule { @Autowired private ApiQueryService apiQueryService; @Autowired private ApiTreeFactory apiTreeFactory; public static final int FIX_RATE=10*1000; /** * 10分钟刷新一次api tree */ @Scheduled(cron ="0 */10 * * * ?") public void refreshApiTree(){ log.info("开始刷新api tree"); ApiTree apiTree= ApiTreeUtils.buildApiTreeFrom(apiQueryService.listEnableApis()); apiTreeFactory.setApiTree(apiTree); log.info("刷新api tree完成"); } } ================================================ FILE: acimage_gateway/src/main/java/com/acimage/gateway/serivce/ApiQueryService.java ================================================ package com.acimage.gateway.serivce; import com.acimage.common.model.domain.sys.Api; import java.util.List; public interface ApiQueryService { List listEnableApis(); } ================================================ FILE: acimage_gateway/src/main/java/com/acimage/gateway/serivce/AuthorizeQueryService.java ================================================ package com.acimage.gateway.serivce; import com.acimage.common.model.domain.sys.Authorize; import java.util.List; import java.util.Map; public interface AuthorizeQueryService { Map> getRolePermissionIdsMap(); List listPermissionIds(Integer roleId); List listAll(); } ================================================ FILE: acimage_gateway/src/main/java/com/acimage/gateway/serivce/RoleQueryService.java ================================================ package com.acimage.gateway.serivce; import java.util.List; public interface RoleQueryService { List listAllIds(); } ================================================ FILE: acimage_gateway/src/main/java/com/acimage/gateway/serivce/UserRoleQueryService.java ================================================ package com.acimage.gateway.serivce; import java.util.List; public interface UserRoleQueryService { List listRoleIds(long userId); } ================================================ FILE: acimage_gateway/src/main/java/com/acimage/gateway/serivce/impl/ApiQueryQueryServiceImpl.java ================================================ package com.acimage.gateway.serivce.impl; import com.acimage.common.global.enums.MatchRule; import com.acimage.common.model.domain.sys.Api; import com.acimage.gateway.dao.ApiDao; import com.acimage.gateway.serivce.ApiQueryService; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import javax.annotation.Nullable; import java.util.List; @Service public class ApiQueryQueryServiceImpl implements ApiQueryService { @Autowired ApiDao apiDao; @Override public List listEnableApis() { LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); qw.eq(Api::isEnable, true); return apiDao.selectList(qw); } } ================================================ FILE: acimage_gateway/src/main/java/com/acimage/gateway/serivce/impl/AuthorizeQueryServiceImpl.java ================================================ package com.acimage.gateway.serivce.impl; import com.acimage.common.model.domain.sys.Authorize; import com.acimage.common.utils.common.ListUtils; import com.acimage.common.utils.redis.RedisUtils; import com.acimage.gateway.dao.AuthorizeDao; import com.acimage.gateway.serivce.AuthorizeQueryService; import com.acimage.gateway.serivce.RoleQueryService; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; @Service public class AuthorizeQueryServiceImpl implements AuthorizeQueryService { @Autowired AuthorizeDao authorizeDao; private Map> rolePermissionIdsMap; @Override public Map> getRolePermissionIdsMap() { return rolePermissionIdsMap; } @Override public List listPermissionIds(Integer roleId) { LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); qw.eq(Authorize::getRoleId, roleId) .select(Authorize::getPermissionId); return ListUtils.extract(Authorize::getPermissionId, authorizeDao.selectList(qw)); } @Override public List listAll() { return authorizeDao.selectList(null); } @Scheduled(fixedRate = 5L, timeUnit = TimeUnit.MINUTES) private Map> refreshRoleIdToPermissionIdsMap() { //为了方便直接保存在内存 Map> map = new HashMap<>(); List authorizeList = authorizeDao.selectList(null); for (Authorize authorize : authorizeList) { Integer roleId = authorize.getRoleId(); Integer permissionId = authorize.getPermissionId(); List permissionIds = map.get(roleId); if (permissionIds == null) { List tempList = new ArrayList<>(); tempList.add(permissionId); map.put(roleId, tempList); } else { permissionIds.add(permissionId); } } this.rolePermissionIdsMap = map; return map; } } ================================================ FILE: acimage_gateway/src/main/java/com/acimage/gateway/serivce/impl/RoleQueryServiceImpl.java ================================================ package com.acimage.gateway.serivce.impl; import com.acimage.common.model.domain.sys.Role; import com.acimage.common.utils.common.ListUtils; import com.acimage.gateway.dao.RoleDao; import com.acimage.gateway.serivce.RoleQueryService; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; @Service public class RoleQueryServiceImpl implements RoleQueryService { @Autowired RoleDao roleDao; @Override public List listAllIds(){ LambdaQueryWrapper qw=new LambdaQueryWrapper<>(); qw.select(Role::getId); List roles= roleDao.selectList(qw); return ListUtils.extract(Role::getId,roles); } } ================================================ FILE: acimage_gateway/src/main/java/com/acimage/gateway/serivce/impl/UserRoleQueryQueryServiceImpl.java ================================================ package com.acimage.gateway.serivce.impl; import com.acimage.common.model.domain.sys.UserRole; import com.acimage.common.redis.annotation.QueryRedis; import com.acimage.common.utils.common.ListUtils; import com.acimage.gateway.dao.UserRoleDao; import com.acimage.gateway.serivce.UserRoleQueryService; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; @Service public class UserRoleQueryQueryServiceImpl implements UserRoleQueryService { @Autowired UserRoleDao userRoleDao; @QueryRedis(keyPrefix = "acimage:gateway:permissionIds:userId:",expire = 5L) @Override public List listRoleIds(long userId){ LambdaQueryWrapper qw=new LambdaQueryWrapper<>(); qw.select(UserRole::getRoleId) .eq(UserRole::getUserId,userId); return ListUtils.extract(UserRole::getRoleId,userRoleDao.selectList(qw)); } } ================================================ FILE: acimage_gateway/src/main/resources/application-dev.yml ================================================ server: port: 8070 spring: config: activate: on-profile: - dev redis: host: 192.168.130.128 port: 6379 password: redis lettuce: pool: max-active: 8 max-idle: 8 #最大空闲连接 min-idle: 0 #最小空闲连接 max-wait: 100ms #连接等待时间 cloud: nacos: server-addr: localhost:8848 #nacos 服务地址 discovery: enabled: true #关闭nacos发现,用于调试 ================================================ FILE: acimage_gateway/src/main/resources/application.yml ================================================ spring: profiles: include: common,common-secret active: dev2 application: name: gateway-server datasource: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver cloud: gateway: discovery: locator: false default-filters: - name: RequestRateLimiter #reids版本太低会无法生效 args: redis-rate-limiter.replenishRate: 20 #补充速率 redis-rate-limiter.burstCapacity: 30 #桶容量 key-resolver: '#{@ipKeyResolver}' #这个必须要配置,否则返回403 routes: - id: community-service-route uri: lb://community-service predicates: - Path=/api/community/** - id: image-service-route uri: lb://image-service predicates: - Path=/api/image/** - id: user-service-route uri: lb://user-service predicates: - Path=/api/user/** - id: websocket-route uri: lb:ws://user-service predicates: - Path=/websocket - id: admin-service-route uri: lb://admin-service predicates: - Path=/api/admin/** mybatis-plus: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.acimage.common.model.domain type-handlers-package: com.acimage.common.config.typehandler global-config: db-config: table-prefix: tb_ role: visitorId: 2 userId: 1 ================================================ FILE: acimage_gateway/src/main/resources/logback-spring.xml ================================================ %d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%20.20thread{20}] %40logger{40} : %msg%n INFO ACCEPT DENY ${LOG_HOME}/${spring.application.name}/%d{yyyy-MM-dd}-info.%i.log 5MB 8 %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n WARN ACCEPT DENY ${LOG_HOME}/${spring.application.name}/%d{yyyy-MM-dd}-warn.%i.log 5MB 15 %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n ERROR ACCEPT DENY ${LOG_HOME}/${spring.application.name}/%d{yyyy-MM-dd}-error.%i.log 5MB 15 %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n ================================================ FILE: acimage_gateway/src/test/java/com/acimage/gateway/GatewayApplicationTest.java ================================================ package com.acimage.gateway; import com.acimage.gateway.apitree.ApiTree; import com.acimage.gateway.apitree.ApiTreeUtils; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.HttpMethod; import java.util.Arrays; import java.util.List; @SpringBootTest public class GatewayApplicationTest { } ================================================ FILE: acimage_gateway/src/test/java/com/acimage/gateway/apitree/ApiTreeTest.java ================================================ package com.acimage.gateway.apitree; import com.acimage.common.model.domain.sys.Api; import com.acimage.gateway.serivce.ApiQueryService; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.HttpMethod; import java.util.Arrays; import java.util.List; @SpringBootTest public class ApiTreeTest { @Autowired ApiQueryService apiQueryService; @Test public void testApiTree() { ApiTree apiTree = ApiTreeUtils.buildApiTreeFrom(apiQueryService.listEnableApis()); List paths = Arrays.asList("/api/community/topics/operate", "/api/community/topics/query", "/api/topics/community/query", "/api/community/topics/jkhakhsd", "/api/community/topics/xxx"); for (String path : paths) { System.out.println(path + ": " + ApiTreeUtils.getMatchApi(apiTree, path, HttpMethod.GET)); } } @Test public void testApiTree2() { ApiTree apiTree = ApiTreeUtils.buildApiTreeFrom(apiQueryService.listEnableApis()); List paths = Arrays.asList( "/api/community/topics/operate"); for (String path : paths) { Api api = ApiTreeUtils.getMatchApi(apiTree, path, HttpMethod.GET); System.out.println(api); } } } ================================================ FILE: acimage_image/pom.xml ================================================ acimage com.acimage 0.0.1-SNAPSHOT 4.0.0 acimage_image jar acimage_image http://maven.apache.org com.acimage acimage_common 0.0.1-SNAPSHOT com.acimage acimage_feign 0.0.1-SNAPSHOT org.springframework.boot spring-boot-starter-aop com.alibaba druid mysql mysql-connector-java runtime net.coobird thumbnailator org.springframework.boot spring-boot-starter-amqp io.minio minio com.github.ben-manes.caffeine caffeine com.github.nintha webp-imageio-core 0.1.0 system ${pom.basedir}/lib/webp-imageio-core-0.1.0.jar org.springframework.boot spring-boot-maven-plugin true org.apache.maven.plugins maven-surefire-plugin true ================================================ FILE: acimage_image/src/main/java/com/acimage/image/ImageApplication.java ================================================ package com.acimage.image; import lombok.extern.slf4j.Slf4j; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.openfeign.EnableFeignClients; import org.springframework.context.annotation.ComponentScan; import org.springframework.scheduling.annotation.EnableScheduling; @Slf4j @SpringBootApplication @EnableScheduling @EnableDiscoveryClient @EnableFeignClients(basePackages="com.acimage.feign") @MapperScan("com.acimage.image.dao") @ComponentScan(value={"com.acimage"}) public class ImageApplication { public static void main(String[] args) { SpringApplication.run(ImageApplication.class, args); log.info("------------->>>Image启动<<<-------------"); } } ================================================ FILE: acimage_image/src/main/java/com/acimage/image/dao/ImageDao.java ================================================ package com.acimage.image.dao; import cn.hutool.core.lang.Pair; import com.acimage.common.model.domain.image.Image; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; import java.util.List; public interface ImageDao extends BaseMapper { Integer insertList(List images); Integer updateDescription(List> idAndDescriptions); @Select("select * from tb_image where topic_id=#{topicId} order by id") List selectListOrderById(@Param("topicId") long topicId); } ================================================ FILE: acimage_image/src/main/java/com/acimage/image/dao/ImageHashDao.java ================================================ package com.acimage.image.dao; import com.acimage.common.model.domain.image.ImageHash; import com.baomidou.mybatisplus.core.mapper.BaseMapper; public interface ImageHashDao extends BaseMapper { } ================================================ FILE: acimage_image/src/main/java/com/acimage/image/global/consts/MyFileConstants.java ================================================ package com.acimage.image.global.consts; public class MyFileConstants { public static final String IMAGE_FORMAT="jpeg"; public static final String ZIP_FORMAT="zip"; } ================================================ FILE: acimage_image/src/main/java/com/acimage/image/global/consts/TopicImageKeyConstants.java ================================================ package com.acimage.image.global.consts; public class TopicImageKeyConstants { public static final String STRINGKP_TOPIC_IMAGES="acimage:image:images:topicId:"; public static final String STRINGKP_IMAGE="acimage:image:images:imageId:"; } ================================================ FILE: acimage_image/src/main/java/com/acimage/image/global/context/DirectoryContext.java ================================================ package com.acimage.image.global.context; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; public class DirectoryContext { @Value("${my-config.images-directory}") public String imagesDirectory; @Value("${my-config.photos-directory}") public String photosDirectory; public static String IMAGES_DIRECTORY; public static String PHOTOS_DIRECTORY; @PostConstruct void init(){ IMAGES_DIRECTORY= imagesDirectory; PHOTOS_DIRECTORY= photosDirectory; } } ================================================ FILE: acimage_image/src/main/java/com/acimage/image/mq/consumer/SyncImagesConsumer.java ================================================ package com.acimage.image.mq.consumer; import com.acimage.common.global.consts.MqConstants; import com.acimage.common.model.mq.dto.SyncImagesUpdateDto; import com.acimage.common.utils.ExceptionUtils; import com.acimage.common.utils.SpringContextUtils; import com.acimage.image.service.image.ImageMixWriteService; import com.rabbitmq.client.Channel; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.core.Message; import org.springframework.amqp.rabbit.annotation.RabbitHandler; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import javax.annotation.PostConstruct; import java.io.*; @Slf4j @Configuration @RabbitListener(queues = MqConstants.SYNC_IMAGES_QUEUE) public class SyncImagesConsumer { @Autowired ImageMixWriteService imageMixWriteService; @RabbitHandler public void syncImages(Channel channel, Message message, SyncImagesUpdateDto updateDto) { log.info("开始同步话题图片"); try { imageMixWriteService.updateImageAndHash(updateDto); } catch (Exception e) { ExceptionUtils.printIfDev(e); log.error("同步图片消费者任务失败 error:{} data:{}", e.getLocalizedMessage(), updateDto); } finally { String messageBody = new String(message.getBody()); try { channel.basicAck(message.getMessageProperties().getDeliveryTag(), false); } catch (IOException e) { ExceptionUtils.printIfDev(e); log.error("同步图片ack失败 error:{} message:{} data:{}", e.getMessage(), messageBody, updateDto); try { channel.basicReject(message.getMessageProperties().getDeliveryTag(), false); } catch (IOException ex) { ExceptionUtils.printIfDev(e); log.error("同步图片reject失败 error:{} message:{} data:{}", e.getMessage(), messageBody, updateDto); } } } } } ================================================ FILE: acimage_image/src/main/java/com/acimage/image/service/config/DhashTaskPoolConfig.java ================================================ package com.acimage.image.service.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import java.util.concurrent.Executor; @Deprecated @EnableAsync public class DhashTaskPoolConfig { public static final String DHASH_TASK_POOL="dhashTask"; @Bean(name = DHASH_TASK_POOL) public Executor imageDhashExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); //核心线程池大小 executor.setCorePoolSize(4); //最大线程数 executor.setMaxPoolSize(10); //队列容量 executor.setQueueCapacity(100); //活跃时间 executor.setKeepAliveSeconds(60); //线程名字前缀 executor.setThreadNamePrefix("dhash-task"); // 设置线程池关闭的时候等待所有任务都完成再继续销毁其他的Bean executor.setWaitForTasksToCompleteOnShutdown(true); // // 线程池对拒绝任务的处理策略,当线程池没有处理能力的时候,该策略会直接在 execute 方法的调用线程中运行被拒绝的任务;如果执行程序已关闭,则会丢弃该任务 // executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); executor.initialize(); return executor; } } ================================================ FILE: acimage_image/src/main/java/com/acimage/image/service/image/ImageMixWriteService.java ================================================ package com.acimage.image.service.image; import com.acimage.common.model.mq.dto.SyncImagesUpdateDto; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; import java.util.List; public interface ImageMixWriteService { String saveImage(MultipartFile imageFile); void removeTopicPartialImages(long topicId, List imageUrls); void removeTopicImages(long topicId); void updateImageAndHash(SyncImagesUpdateDto updateDto); } ================================================ FILE: acimage_image/src/main/java/com/acimage/image/service/image/ImageQueryService.java ================================================ package com.acimage.image.service.image; import com.acimage.common.model.domain.image.Image; import java.util.List; public interface ImageQueryService { List listImagesOrderById(long topicId); List listImagesByIds(List imageIds); List listImageIds(long topicId, List imageUrls); List listImageIds(long topicId); List listImagesForHavingNullTopicId(List imageUrls); } ================================================ FILE: acimage_image/src/main/java/com/acimage/image/service/image/ImageWriteService.java ================================================ package com.acimage.image.service.image; import com.acimage.common.model.domain.image.Image; import com.baomidou.mybatisplus.extension.service.IService; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; import java.util.List; public interface ImageWriteService extends IService { @Deprecated void saveImages(List images); Image saveImage(String url,int size,String fileName); void removeImages(long topicId); int removeImages(long topicId, List imageUrls); void updateTopicId(List imageIds,long topicId); void updateTopicIdForHavingNullTopicId(List imageIds, long topicId); } ================================================ FILE: acimage_image/src/main/java/com/acimage/image/service/image/impl/ImageMixWriteServiceImpl.java ================================================ package com.acimage.image.service.image.impl; import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.util.IdUtil; import com.acimage.common.global.consts.StorePrefixConstants; import com.acimage.common.global.exception.BusinessException; import com.acimage.common.global.consts.FileFormatConstants; import com.acimage.common.model.domain.image.Image; import com.acimage.common.model.mq.dto.SyncImagesUpdateDto; import com.acimage.common.utils.IdGenerator; import com.acimage.common.utils.ImageUtils; import com.acimage.common.utils.common.ListUtils; import com.acimage.common.utils.minio.MinioUtils; import com.acimage.image.service.image.ImageMixWriteService; import com.acimage.image.service.image.ImageQueryService; import com.acimage.image.service.image.ImageWriteService; import com.acimage.image.service.imagehash.ImageHashWriteService; import com.acimage.image.service.imagehash.SearchImageService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; import javax.annotation.PostConstruct; import java.io.*; import java.util.Date; import java.util.List; @Slf4j @Service public class ImageMixWriteServiceImpl implements ImageMixWriteService { public String tempDirectory; @PostConstruct public void init() { tempDirectory = System.getProperty("user.dir") + "//temp"; //创建目录 File directory = new File(tempDirectory); if (directory.mkdir()) { log.info("创建临时目录:{}", tempDirectory); } } @Autowired ImageWriteService imageWriteService; @Autowired ImageQueryService imageQueryService; @Autowired MinioUtils minioUtils; @Autowired ImageHashWriteService imageHashWriteService; @Autowired SearchImageService searchImageService; @Override public String saveImage(MultipartFile imageFile) { long imageId = IdGenerator.getSnowflakeNextId(); String suffix = String.format("%s.%s", imageId, FileFormatConstants.WEBP); String url = minioUtils.generateBaseUrl(StorePrefixConstants.TOPIC_IMAGE, new Date(), suffix); //压缩为webp,压缩后不超过200kb int limitSize = 200 * 1000; int limitLength=1000; int size; String totalUrl; try (InputStream inputStream = ImageUtils.compressAsWebpImage(imageFile, limitSize,limitLength)) { size = inputStream.available(); //上传 totalUrl = minioUtils.upload(inputStream, url, FileFormatConstants.WEBP_CONTENT_TYPE); }catch (IOException e) { log.error(e.getMessage()); throw new BusinessException("文件上传失败"); } //保存到数据库 String fileName = imageFile.getOriginalFilename(); imageWriteService.saveImage(totalUrl, size, fileName); return totalUrl; } @Override public void removeTopicPartialImages(long topicId, List imageUrls) { if (CollectionUtil.isEmpty(imageUrls)) { return; } //找到要删除的图片id List imageIds = imageQueryService.listImageIds(topicId, imageUrls); //删除图片 imageWriteService.removeImages(topicId, imageUrls); //删除图片哈希 imageHashWriteService.removeImageHashes(imageIds); } @Override public void removeTopicImages(long topicId) { //找到要删除的图片id List imageIds = imageQueryService.listImageIds(topicId); //删除图片 imageWriteService.removeImages(topicId); //删除图片哈希 imageHashWriteService.removeImageHashes(imageIds); } @Override public void updateImageAndHash(SyncImagesUpdateDto updateDto) { long topicId = updateDto.getTopicId(); switch (updateDto.getServiceType()) { case ADD: case UPDATE: log.info("开始哈希图片 {}", updateDto); List addImageUrlList = updateDto.getAddImageUrls(); //获取实际存在的图片 List images = imageQueryService.listImagesForHavingNullTopicId(addImageUrlList); for (Image image : images) { //下载图片到本地 String tempFilePath = String.format("%s/%s", tempDirectory, image.getId() + IdUtil.fastSimpleUUID()); try { minioUtils.downloadTo(image.getUrl(), tempFilePath); } catch (Exception e) { log.error("哈希图片时下载出错 error:{} url:{}", e.getLocalizedMessage(), image.getUrl()); continue; } File tempFile = new File(tempFilePath); FileInputStream is = null; try { is = new FileInputStream(tempFilePath); //将图片哈希并存到数据库 searchImageService.hashImageByDhashAlgorithm(is, image.getId()); } catch (FileNotFoundException e) { log.error("系统错误 文件不存在{}", tempFilePath); return; } finally { if (is != null) { try { is.close(); } catch (IOException e) { log.error("inputStream 关闭失败 {}", e.getMessage()); throw new RuntimeException(e); } } } tempFile.delete(); } //更新图片对应话题id List imageIds = ListUtils.extract(Image::getId, images); imageWriteService.updateTopicIdForHavingNullTopicId(imageIds, topicId); //删除图片 List removeImageUrlList = updateDto.getRemoveImageUrls(); this.removeTopicPartialImages(topicId, removeImageUrlList); break; case DELETE: this.removeTopicImages(topicId); break; } } } ================================================ FILE: acimage_image/src/main/java/com/acimage/image/service/image/impl/ImageQueryServiceImpl.java ================================================ package com.acimage.image.service.image.impl; import cn.hutool.core.collection.CollectionUtil; import com.acimage.common.model.domain.image.Image; import com.acimage.common.redis.annotation.QueryRedis; import com.acimage.common.utils.common.ListUtils; import com.acimage.image.dao.ImageDao; import com.acimage.image.service.image.ImageQueryService; import com.acimage.image.global.consts.TopicImageKeyConstants; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.List; @Service public class ImageQueryServiceImpl implements ImageQueryService { @Autowired ImageDao imageDao; @QueryRedis(keyPrefix = TopicImageKeyConstants.STRINGKP_TOPIC_IMAGES, expire = 3L) @Override public List listImagesOrderById(long topicId) { LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); qw.select(Image::getUrl,Image::getSize,Image::getId,Image::getTopicId) .orderByAsc(Image::getId) .eq(Image::getTopicId, topicId); return imageDao.selectList(qw); } @Override public List listImagesByIds(List imageIds) { if(CollectionUtil.isEmpty(imageIds)){ return new ArrayList<>(); } LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); qw.select(Image::getUrl,Image::getSize,Image::getId,Image::getTopicId) .in(Image::getId, imageIds); List images = imageDao.selectList(qw); //按照给出的imageIds顺序排好 List orderedImages = new ArrayList<>(); for (Long imageId : imageIds) { for (Image image : images) { if (imageId.equals(image.getId())) { orderedImages.add(image); } } } return orderedImages; } @Override public List listImageIds(long topicId, List imageUrls) { if(CollectionUtil.isEmpty(imageUrls)){ return new ArrayList<>(); } LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); qw.in(Image::getUrl, imageUrls) .eq(Image::getTopicId,topicId) .select(Image::getId); List images = imageDao.selectList(qw); return ListUtils.extract(Image::getId,images); } @Override public List listImageIds(long topicId) { LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); qw.eq(Image::getTopicId,topicId) .select(Image::getId); List images = imageDao.selectList(qw); return ListUtils.extract(Image::getId,images); } @Override public List listImagesForHavingNullTopicId(List imageUrls){ if(CollectionUtil.isEmpty(imageUrls)){ return new ArrayList<>(); } LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); qw.in(Image::getUrl, imageUrls) .isNull(Image::getTopicId); List images = imageDao.selectList(qw); return images; } } ================================================ FILE: acimage_image/src/main/java/com/acimage/image/service/image/impl/ImageWriteServiceImpl.java ================================================ package com.acimage.image.service.image.impl; import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.lang.Pair; import cn.hutool.core.util.StrUtil; import com.acimage.common.global.context.UserContext; import com.acimage.common.model.domain.image.Image; import com.acimage.common.utils.IdGenerator; import com.acimage.common.deprecated.QiniuUtils; import com.acimage.common.utils.minio.MinioUtils; import com.acimage.common.utils.redis.RedisUtils; import com.acimage.common.utils.common.FileUtils; import com.acimage.common.utils.common.PairUtils; import com.acimage.image.dao.ImageDao; import com.acimage.image.service.image.ImageQueryService; import com.acimage.image.service.image.ImageWriteService; import com.acimage.image.global.consts.TopicImageKeyConstants; import com.acimage.image.service.imagehash.SearchImageService; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; import reactor.util.annotation.Nullable; import javax.validation.constraints.NotNull; import java.util.ArrayList; import java.util.Date; import java.util.List; @Slf4j @Service public class ImageWriteServiceImpl extends ServiceImpl implements ImageWriteService { @Autowired ImageDao imageDao; @Autowired ImageQueryService imageQueryService; @Autowired(required = false) QiniuUtils qiniuUtils; @Autowired RedisUtils redisUtils; @Override public void saveImages(@Nullable List images) { if (images == null || images.size() == 0) { return; } imageDao.insertList(images); } @Override public Image saveImage(String url, int size, String fileName) { long imageId = IdGenerator.getSnowflakeNextId(); Date now = new Date(); Image image = Image.builder() .url(url) .id(imageId) .size(size) .fileName(fileName) .description(fileName) .createTime(now) .updateTime(now) .build(); imageDao.insert(image); return image; } @Override public void removeImages(long topicId) { LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); qw.eq(Image::getTopicId, topicId); imageDao.delete(qw); //删除话题图片 redisUtils.delete(TopicImageKeyConstants.STRINGKP_TOPIC_IMAGES); } @Override public int removeImages(long topicId, List imageUrls) { LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); qw.in(Image::getUrl, imageUrls) .eq(Image::getTopicId, topicId); return imageDao.delete(qw); } @Override public void updateTopicId(List imageIds, long topicId) { if (CollectionUtil.isEmpty(imageIds)) { return; } LambdaUpdateWrapper uw = new LambdaUpdateWrapper<>(); uw.in(Image::getId, imageIds) .set(Image::getTopicId, topicId); imageDao.update(null, uw); } @Override public void updateTopicIdForHavingNullTopicId(List imageIds, long topicId) { if (CollectionUtil.isEmpty(imageIds)) { return; } LambdaUpdateWrapper uw = new LambdaUpdateWrapper<>(); uw.in(Image::getId, imageIds) .isNull(Image::getTopicId) .set(Image::getTopicId, topicId); imageDao.update(null, uw); } } ================================================ FILE: acimage_image/src/main/java/com/acimage/image/service/imagehash/ImageHashWriteService.java ================================================ package com.acimage.image.service.imagehash; import java.io.InputStream; import java.util.List; public interface ImageHashWriteService { void removeImageHashes(List imageIds); void HashImagesByDhash(InputStream imageInputStream, long imageId) ; } ================================================ FILE: acimage_image/src/main/java/com/acimage/image/service/imagehash/SearchImageService.java ================================================ package com.acimage.image.service.imagehash; import com.acimage.common.model.domain.image.Image; import org.springframework.web.multipart.MultipartFile; import java.io.InputStream; import java.util.List; public interface SearchImageService { @Deprecated void processImagesHashForNotProcessedImages(); void hashImageByDhashAlgorithm(InputStream imageInputStream, long imageId) ; List searchMostSimilarImages(MultipartFile imageFile); void removeImageHashes(List imageIds); } ================================================ FILE: acimage_image/src/main/java/com/acimage/image/service/imagehash/impl/ImageHashWriteServiceImpl.java ================================================ package com.acimage.image.service.imagehash.impl; import cn.hutool.core.collection.CollectionUtil; import com.acimage.common.utils.ExceptionUtils; import com.acimage.image.dao.ImageHashDao; import com.acimage.common.model.domain.image.ImageHash; import com.acimage.image.service.imagehash.ImageHashWriteService; import com.acimage.image.utils.BitUtils; import com.acimage.image.utils.DhashUtils; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DuplicateKeyException; import org.springframework.stereotype.Service; import java.io.IOException; import java.io.InputStream; import java.util.List; @Service @Slf4j public class ImageHashWriteServiceImpl implements ImageHashWriteService { @Autowired ImageHashDao imageHashDao; @Override public void removeImageHashes(List imageIds) { if (CollectionUtil.isEmpty(imageIds)) { return; } imageHashDao.deleteBatchIds(imageIds); } @Override public void HashImagesByDhash(InputStream imageInputStream, long imageId) { long hashValue; try { hashValue = DhashUtils.getImageDhashFrom(imageInputStream); } catch (IOException e) { log.error("imageId:{} 对应文件IO异常", imageId); ExceptionUtils.printIfDev(e); throw new RuntimeException(e); } int hashSum = BitUtils.sumOfBits(hashValue); try { imageHashDao.insert(new ImageHash(imageId, hashValue, hashSum)); } catch (DuplicateKeyException e) { log.error("保存图片哈希值时,插入数据库imageId:{}重复", imageId); } } } ================================================ FILE: acimage_image/src/main/java/com/acimage/image/service/imagehash/impl/SearchImageServiceImpl.java ================================================ package com.acimage.image.service.imagehash.impl; import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.util.StrUtil; import com.acimage.common.global.context.UserContext; import com.acimage.common.global.exception.BusinessException; import com.acimage.common.model.domain.image.Image; import com.acimage.common.model.domain.community.Topic; import com.acimage.common.utils.ExceptionUtils; import com.acimage.common.utils.common.ListUtils; import com.acimage.feign.client.TopicClient; import com.acimage.image.dao.ImageHashDao; import com.acimage.image.global.context.DirectoryContext; import com.acimage.common.model.domain.image.ImageHash; import com.acimage.image.service.image.ImageQueryService; import com.acimage.image.service.imagehash.SearchImageService; import com.acimage.image.utils.BitUtils; import com.acimage.image.utils.DhashUtils; import com.acimage.image.utils.ImageFileUtils; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DuplicateKeyException; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; import java.io.*; import java.util.ArrayList; import java.util.Comparator; import java.util.List; @Slf4j @Service public class SearchImageServiceImpl implements SearchImageService { @Autowired ImageHashDao imageDhashDao; @Autowired ImageQueryService imageQueryService; @Autowired TopicClient topicClient; /** * 利用dhash算法处理未被处理过的图片文件,保存到数据库中,在高并发环境下可能会有重复处理的问题 * 废弃原因:图片存到七牛云,而不是本地了 */ @Override @Deprecated public void processImagesHashForNotProcessedImages() { //获取所有保存的图片 String directoryPath = DirectoryContext.IMAGES_DIRECTORY; File directory = new File(directoryPath); File[] files; if (directory.isDirectory()) { files = directory.listFiles(); } else { log.error("{} 不是目录或不存在", directoryPath); return; } if (files == null) { return; } //获取数据库中已经被处理过的图片 List imageHashList = imageDhashDao.selectList(null); List imageIdsInDb = ListUtils.extract(ImageHash::getImageId, imageHashList); List imageIdsInDirectory = new ArrayList<>(); for (File file : files) { String stringId = StrUtil.subBefore(file.getName(), '.', true); if (stringId.length() > 12) { //排除掉默认图片0.jpeg等 imageIdsInDirectory.add(Long.parseLong(stringId)); } } //得到没被处理过的图片 List differenceList = ListUtils.differenceSetOf(imageIdsInDirectory, imageIdsInDb); //将图片dhash值保存到数据库中 for (Long imageId : differenceList) { String filePath = ImageFileUtils.imageIdToImagePath(imageId); File file = new File(filePath); try { //此处是同步进行,因为该方法非代理对象的方法 hashImageByDhashAlgorithm(new FileInputStream(file), imageId); } catch (FileNotFoundException e1) { log.error("提取图片dhash时 文件:{}不存在", filePath); } } } @Override public void hashImageByDhashAlgorithm(InputStream imageInputStream, long imageId) { long hashValue; try { hashValue = DhashUtils.getImageDhashFrom(imageInputStream); } catch (IOException e) { log.error("imageId:{} 对应文件IO异常", imageId); ExceptionUtils.printIfDev(e); throw new RuntimeException(e); } int hashSum = BitUtils.sumOfBits(hashValue); try { imageDhashDao.insert(new ImageHash(imageId, hashValue, hashSum)); } catch (DuplicateKeyException e) { log.error("保存图片dhash时,插入数据库imageId:{}重复", imageId); } } @Override public List searchMostSimilarImages(MultipartFile imageFile) { int rankEnd = 10; final int threshold = 20; InputStream inputStream = null; try { inputStream = imageFile.getInputStream(); } catch (IOException e) { log.error("用户:{} 以图搜图 错误:传入文件getInputStream异常", UserContext.getUsername()); try { inputStream.close(); } catch (IOException ex) { log.error(e.getMessage()); throw new RuntimeException(ex); } throw new BusinessException("文件IO异常"); } long hashValue; try { hashValue = DhashUtils.getImageDhashFrom(inputStream); } catch (IOException e) { try { inputStream.close(); } catch (IOException ex) { log.error(e.getMessage()); throw new RuntimeException(ex); } throw new BusinessException("文件IO异常"); } List imageHashList = imageDhashDao.selectList(null); List resultList = new ArrayList<>(); for (ImageHash imageHash : imageHashList) { int distance = DhashUtils.distanceBetween(hashValue, imageHash.getHashValue()); imageHash.setDistance(distance); if (distance <= threshold) { resultList.add(imageHash); } } resultList.sort(Comparator.comparing(ImageHash::getDistance)); int toIndex = Math.min(rankEnd, resultList.size()); log.debug("搜索结果:{}", resultList); //找到图片对象 List imageIds = ListUtils.extract(ImageHash::getImageId, resultList.subList(0, toIndex)); List images = imageQueryService.listImagesByIds(imageIds); if (CollectionUtil.isEmpty(images)) { return new ArrayList<>(); } //找到对应话题 List topicIds = ListUtils.extract(Image::getTopicId, images); List topics = topicClient.queryTopics(topicIds).getData(); List imageWithTopics = new ArrayList<>(); for (Image image : images) { for (Topic topic : topics) { if (topic.getId().equals(image.getTopicId())) { image.setTopic(topic); imageWithTopics.add(image); } } } if (inputStream != null) { try { inputStream.close(); } catch (IOException e) { throw new RuntimeException(e); } } return imageWithTopics; } @Override public void removeImageHashes(List imageIds) { imageDhashDao.deleteBatchIds(imageIds); } } ================================================ FILE: acimage_image/src/main/java/com/acimage/image/service/photo/PhotoService.java ================================================ package com.acimage.image.service.photo; import org.springframework.web.multipart.MultipartFile; public interface PhotoService { String uploadPhotoAndUpdatePhotoUrl(MultipartFile photoFile); } ================================================ FILE: acimage_image/src/main/java/com/acimage/image/service/photo/impl/PhotoServiceImpl.java ================================================ package com.acimage.image.service.photo.impl; import com.acimage.common.global.exception.BusinessException; import com.acimage.common.global.consts.FileFormatConstants; import com.acimage.common.result.Result; import com.acimage.common.utils.IdGenerator; import com.acimage.common.utils.ImageUtils; import com.acimage.common.utils.minio.MinioUtils; import com.acimage.feign.client.UserClient; import com.acimage.common.global.consts.StorePrefixConstants; import com.acimage.image.service.photo.PhotoService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; import java.io.InputStream; import java.util.Date; @Service public class PhotoServiceImpl implements PhotoService { @Autowired UserClient userClient; @Autowired MinioUtils minioUtils; /** * 返回新token */ @Override public String uploadPhotoAndUpdatePhotoUrl(MultipartFile photoFile){ //上传头像到七牛云 Date now = new Date(); String suffix = String.format("%s.%s", IdGenerator.getSnowflakeNextId(), FileFormatConstants.WEBP); //压缩后限制大小 int limitSize=20*1000; int width=200; int height=200; InputStream inputStream= ImageUtils.compressAsFixedWebpImage(photoFile,width,height,limitSize); //上传 String photoUrl = minioUtils.generateBaseUrl(StorePrefixConstants.USER_PHOTO, now, suffix); photoUrl=minioUtils.upload(inputStream, photoUrl, FileFormatConstants.WEBP_CONTENT_TYPE); Result result=userClient.modifyPhotoUrl(photoUrl); if(result.isOk()){ return result.getData(); }else{ throw new BusinessException("头像修改失败"); } } } ================================================ FILE: acimage_image/src/main/java/com/acimage/image/utils/BitUtils.java ================================================ package com.acimage.image.utils; public class BitUtils { public static int getBit(long value, int index) { if (index < 0 || index > 63) { throw new IllegalArgumentException("index范围为0-63"); } if (index == 63) { if (value < 0) { return 1; } else { return 0; } } else { if (value < 0) { //使value变为value补码去掉符号位之后的值 value = value + (1L << 62) + (1L << 62); } return (int) ((value >> index) & 1); } } /** * * @param str64 64位补码 * @return 补码对应的long值 */ public static long str64ToLong(String str64){ if(str64==null||str64.length()!=64){ throw new IllegalArgumentException("str64传参为:"+str64+";str64必须为64位长度的0-1字符串!"); } int sgn=str64.charAt(0)=='1'?-1:1; //获取符合外之外的值 long sum=0; for(int i=0;i<63;i++){ long bit=0L; if(str64.charAt(63-i)=='0'){ bit=0L; } else if (str64.charAt(63 - i) == '1') { bit =1L; }else{ throw new IllegalArgumentException("str64传参为:"+str64+";str64必须为64位长度的0-1字符串!"); } sum+=(bit< inputStreams = Collections.singletonList(inputStream); Thumbnails.Builder inputStreamBuilder = Thumbnails.fromInputStreams(inputStreams).forceSize(width, height); BufferedImage bufferedImage = inputStreamBuilder.asBufferedImage(); StringBuilder str64 = new StringBuilder((width - 1) * height); int[][] grays = new int[width][height]; for (int hi = 0; hi < height; hi++) { for (int wi = 0; wi < width; wi++) { Color color = new Color(bufferedImage.getRGB(wi, hi)); grays[wi][hi] = ImageUtils.rgb2Gray(color.getRed(), color.getGreen(), color.getBlue()); if (wi > 0) { int hashBit = 0; if (grays[wi][hi] > grays[wi - 1][hi]) { hashBit = 1; } str64.append(hashBit); } } } return BitUtils.str64ToLong(str64.toString()); } public static int distanceBetween(@NotNull InputStream inputStream1, @NotNull InputStream inputStream2) throws IOException { String str1 = BitUtils.longToStr64(getImageDhashFrom(inputStream1)); String str2 = BitUtils.longToStr64(getImageDhashFrom(inputStream2)); return hammingDistanceBetween(str1,str2); } public static int distanceBetween(long hashValue1, long hashValue2) { String str1 = BitUtils.longToStr64(hashValue1); String str2 = BitUtils.longToStr64(hashValue2); return hammingDistanceBetween(str1,str2); } private static int hammingDistanceBetween(@NotNull String str1, @NotNull String str2) { int distance = 0; if (str1.length() != str2.length()) { throw new IllegalArgumentException("参数长度不一致"); } for (int i = 0; i < str1.length(); i++) { if (str1.charAt(i) != str2.charAt(i)) { distance++; } } return distance; } } ================================================ FILE: acimage_image/src/main/java/com/acimage/image/utils/ImageFileUtils.java ================================================ package com.acimage.image.utils; import com.acimage.image.global.consts.MyFileConstants; import com.acimage.image.global.context.DirectoryContext; import javax.validation.constraints.NotNull; public class ImageFileUtils { public static String imageIdToImagePath(@NotNull Long imageId){ return DirectoryContext.IMAGES_DIRECTORY+"/"+imageId+"."+ MyFileConstants.IMAGE_FORMAT; } } ================================================ FILE: acimage_image/src/main/java/com/acimage/image/utils/ImageUtils.java ================================================ package com.acimage.image.utils; public class ImageUtils { public static int rgb2Gray(int r, int g, int b) { return (int) Math.round(r * 0.299 + g * 0.581 + b * 0.114); } } ================================================ FILE: acimage_image/src/main/java/com/acimage/image/web/config/WebMvcConfig.java ================================================ package com.acimage.image.web.config; import com.acimage.common.web.interceptor.JwtInterceptor; import com.acimage.common.web.interceptor.AccessInterceptor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @Slf4j @Configuration public class WebMvcConfig implements WebMvcConfigurer { @Autowired JwtInterceptor jwtInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { List commonExcludePathPatterns = new ArrayList<>( Arrays.asList("/templates/**", "/static/**", "/", "/storage/**", "/favicon.ico", "/error")); registry.addInterceptor(jwtInterceptor).addPathPatterns("/**") .excludePathPatterns(commonExcludePathPatterns).order(20); registry.addInterceptor(new AccessInterceptor()).addPathPatterns("/**") .excludePathPatterns(commonExcludePathPatterns).order(30); } } ================================================ FILE: acimage_image/src/main/java/com/acimage/image/web/controller/ImageOperateController.java ================================================ package com.acimage.image.web.controller; import com.acimage.common.deprecated.annotation.Authentication; import com.acimage.common.global.consts.FileFormatConstants; import com.acimage.common.global.consts.TimeConstants; import com.acimage.common.global.context.UserContext; import com.acimage.common.redis.annotation.RequestLimit; import com.acimage.common.redis.enums.LimitTarget; import com.acimage.common.result.Result; import com.acimage.common.utils.common.FileUtils; import com.acimage.image.service.image.ImageMixWriteService; import com.acimage.image.service.image.ImageWriteService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; @RestController @Slf4j @Validated @Authentication @RequestMapping("/api/image/images/operate") public class ImageOperateController { @Autowired ImageMixWriteService imageMixWriteService; @RequestLimit(limitTimes = {1, 20}, durations = {2, TimeConstants.DAY_SECONDS}, penaltyTimes = {-1, -1}, targets = {LimitTarget.IP, LimitTarget.USER}) @PostMapping("/upload/topicImage") public Result uploadTopicImage(@RequestParam("imageFile") MultipartFile imageFile) { String originName = imageFile.getOriginalFilename(); String format = FileUtils.formatOf(originName); if (!FileFormatConstants.ALLOWED_IMAGE_FORMAT.contains(format)) { return Result.fail("图片格式需为" + FileFormatConstants.ALLOWED_IMAGE_FORMAT); } log.info("用户:{} 话题: 上传话题图片", UserContext.getUsername()); return Result.ok(imageMixWriteService.saveImage(imageFile)); } } ================================================ FILE: acimage_image/src/main/java/com/acimage/image/web/controller/PhotoOperateController.java ================================================ package com.acimage.image.web.controller; import com.acimage.common.deprecated.annotation.Authentication; import com.acimage.common.global.consts.TimeConstants; import com.acimage.common.redis.annotation.RequestLimit; import com.acimage.common.redis.enums.LimitTarget; import com.acimage.common.result.Result; import com.acimage.image.service.photo.impl.PhotoServiceImpl; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; @RestController @Slf4j @Validated @Authentication @RequestMapping("/api/image/photos/operate") public class PhotoOperateController { @Autowired PhotoServiceImpl photoService; @RequestLimit(limitTimes = {1,3}, durations = {2, TimeConstants.DAY_SECONDS}, penaltyTimes = {-1,-1}, targets = {LimitTarget.IP,LimitTarget.USER}) @PostMapping("/upload") public Result uploadPhoto(@RequestParam("photoFile") MultipartFile photoFile) { String url = photoService.uploadPhotoAndUpdatePhotoUrl(photoFile); return Result.ok(url); } } ================================================ FILE: acimage_image/src/main/java/com/acimage/image/web/controller/SearchImageController.java ================================================ package com.acimage.image.web.controller; import com.acimage.common.deprecated.annotation.Authentication; import com.acimage.common.model.domain.image.Image; import com.acimage.common.redis.annotation.RequestLimit; import com.acimage.common.redis.enums.LimitTarget; import com.acimage.common.result.Result; import com.acimage.image.service.imagehash.SearchImageService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import java.util.List; @RestController @Slf4j @Validated @Authentication @RequestMapping("/api/image/images") public class SearchImageController { @Autowired SearchImageService searchImageService; @RequestLimit(limitTimes = {1,10}, durations = {3,1}, penaltyTimes = {-1,-1}, targets = {LimitTarget.IP,LimitTarget.ALL}) @PostMapping("/searchByImage") public Result> searchImageWithTopicByImage(@RequestParam("imageFile") MultipartFile multipartFile) { return Result.ok(searchImageService.searchMostSimilarImages(multipartFile)); } } ================================================ FILE: acimage_image/src/main/java/com/acimage/image/web/provider/ImageProvider.java ================================================ package com.acimage.image.web.provider; import com.acimage.common.deprecated.annotation.Authentication; import com.acimage.common.global.enums.AuthenticationType; import com.acimage.common.model.domain.image.Image; import com.acimage.common.result.Result; import com.acimage.image.service.image.ImageMixWriteService; import com.acimage.image.service.image.ImageQueryService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import java.util.List; @RestController @Slf4j @Validated @RequestMapping("/image/images") public class ImageProvider { @Autowired ImageMixWriteService imageMixWriteService; @Autowired ImageQueryService imageQueryService; @GetMapping("/topicId/{topicId}") public Result> queryTopicImages(@PathVariable Long topicId) { return Result.ok(imageQueryService.listImagesOrderById(topicId)); } } ================================================ FILE: acimage_image/src/main/resources/application-dev.yml ================================================ server: port: 8090 spring: config: activate: on-profile: - dev datasource: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/acimage_image?useSSl=false&allowMultiQueries=true&serverTimezone=UTC username: root password: mysql rabbitmq: host: 192.168.130.128 port: 5672 username: acimage password: acimage virtual-host: /acimage listener: simple: acknowledge-mode: manual # 手动应答 auto-startup: false #消费者是否自动启动 prefetch: 1 #每次从队列中取一个,轮询分发,默认是公平分发 retry: max-attempts: 5 # 重试次数 enabled: true # 开启重试 direct: auto-startup: false #生产者是否自动启动 acknowledge-mode: manual # 手动应答 redis: host: 192.168.130.128 port: 6379 password: redis lettuce: pool: max-active: 8 max-idle: 8 #最大空闲连接 min-idle: 0 #最小空闲连接 max-wait: 100ms #连接等待时间 cloud: nacos: server-addr: localhost:8848 #nacos 服务地址 ================================================ FILE: acimage_image/src/main/resources/application.yml ================================================ spring: profiles: include: common,common-secret active: dev2 application: name: image-service servlet: multipart: max-file-size: 2MB max-request-size: 4MB feign: okhttp: enabled: true httpclient: max-connections: 20 # 最大的连接数 max-connections-per-route: 5 # 每个路径的最大连接数 mybatis-plus: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.acimage.image.model.domain,com.acimage.common.model.domain configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl global-config: db-config: table-prefix: tb_ ================================================ FILE: acimage_image/src/main/resources/logback-spring.xml ================================================ %d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%20.20thread{20}] %40logger{40} : %msg%n INFO ACCEPT DENY ${LOG_HOME}/${spring.application.name}/%d{yyyy-MM-dd}-info.%i.log 5MB 8 %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n WARN ACCEPT DENY ${LOG_HOME}/${spring.application.name}/%d{yyyy-MM-dd}-warn.%i.log 5MB 15 %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n ERROR ACCEPT DENY ${LOG_HOME}/${spring.application.name}/%d{yyyy-MM-dd}-error.%i.log 5MB 15 %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n ================================================ FILE: acimage_image/src/main/resources/mapper/ImageMapper.xml ================================================ insert into tb_image(id,topic_id,size,description,url) values ( #{image.id},#{image.topicId},#{image.size},#{image.description},#{image.url} ) update tb_image set description=#{idAndDescription.value} where id=#{idAndDescription.key} ================================================ FILE: acimage_image/src/test/java/com/acimage/image/CasualTest.java ================================================ package com.acimage.image; import cn.hutool.core.date.DateUtil; import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.crypto.digest.DigestUtil; import com.acimage.common.utils.common.ListUtils; import com.acimage.image.utils.BitUtils; import com.acimage.image.utils.DhashUtils; import net.coobird.thumbnailator.Thumbnails; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import javax.imageio.ImageIO; import java.awt.*; import java.awt.image.BufferedImage; import java.io.*; import java.util.*; import java.util.List; import java.util.regex.Pattern; @SpringBootTest public class CasualTest { @Test public void testGetSnowflakeId(){ long id=IdUtil.getSnowflake().nextId(); System.out.println(id); System.out.println(Long.toString(id).length()); System.out.println(); } @Test public void testMd5(){ String txt="b3cfd02862e5565ebecfa608035911d612346546"; System.out.println(DigestUtil.md5Hex(txt).length()); } @Test public void testUUID(){ String uuid=IdUtil.simpleUUID(); System.out.println(uuid.length()); } @Test public void testPatternMatch(){ final String PASSWORD_PATTERN="^(?![a-zA-Z]+$)[0-9A-Za-z]{6,16}$"; String password="5555555----"; System.out.println(Pattern.matches(PASSWORD_PATTERN,password)); } @Test public void testDateNow(){ Date beginOfDay = DateUtil.beginOfDay(new Date()); //获取昨天开始时间 Date startDate=DateUtil.offsetDay(beginOfDay,-1); String startTime=DateUtil.format(startDate,"yyyy-MM-dd HH:mm:ss"); System.out.println(startTime); } @Test public void testSubAfter(){ String str="558.968.jpeg"; System.out.println(StrUtil.subAfter(str,'.',true)); List s= Arrays.asList("jpg","png"); System.out.println(s.contains("jpg")); } @Test public void testTh(){ BufferedImage inputStream= null; try { inputStream = ImageIO.read(new FileInputStream("F:\\MyImage\\素材\\bg2.png")); // inputStream = new FileInputStream("F:\\MyImage\\素材\\1.jpeg"); List inputStreams=new ArrayList<>(List.of(inputStream)); Thumbnails.Builder inputStreamBuilder=Thumbnails.fromImages(inputStreams); inputStreamBuilder.forceSize(9,8); // inputStreamBuilder.asBufferedImage().toFile(new File("F:\\MyImage\\素材\\0001.jpeg")); ImageIO.write(inputStreamBuilder.asBufferedImage(),"jpeg",new File("F:\\MyImage\\素材\\0bg2.png")); // inputStreamBuilder.asFiles(new ArrayList<>(List.of(new File("F:\\MyImage\\素材\\0001.jpeg")))); } catch (IOException e) { throw new RuntimeException(e); } } @Test public void testGetImageDhashFrom(){ InputStream inputStream1= null; InputStream inputStream2= null; try { inputStream1 = new FileInputStream("F:\\MyImage\\素材\\1.jpeg"); inputStream2 = new FileInputStream("F:\\MyImage\\素材\\0001.jpeg"); System.out.println(DhashUtils.distanceBetween(inputStream1,inputStream2)); } catch (IOException e) { throw new RuntimeException(e); } } @Test public void testImageScale(){ Image img=Toolkit.getDefaultToolkit().getImage(System.getProperty("user.dir")); InputStream inputStream1= null; InputStream inputStream2= null; try { inputStream1 = new FileInputStream("F:\\MyImage\\素材\\0001.jpeg"); inputStream2 = new FileInputStream("F:\\MyImage\\素材\\1.jpeg"); System.out.println(DhashUtils.distanceBetween(inputStream1,inputStream2)); } catch (IOException e) { throw new RuntimeException(e); } } @Test public void testBitUtils(){ long a=18588L; System.out.println(BitUtils.longToStr64(a)); System.out.println(BitUtils.sumOfBits(a)); System.out.println(BitUtils.str64ToLong(BitUtils.longToStr64(a))); new ArrayList<>(Arrays.asList()).subList(0,10); } @Test public void test89(){ List list1=new ArrayList<>(Arrays.asList(1L,2L,5L,7L,10089L,999999L)); List list2=new ArrayList<>(Arrays.asList(2L,4L,6L,7L,10086L)); System.out.println(ListUtils.differenceSetOf(list1,list2)); } } ================================================ FILE: acimage_image/src/test/java/com/acimage/image/ImageApplicationTests.java ================================================ package com.acimage.image; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest class ImageApplicationTests { @Test void contextLoads() { } } ================================================ FILE: acimage_image/src/test/java/com/acimage/image/dao/ImageDaoTest.java ================================================ package com.acimage.image.dao; import com.acimage.common.model.domain.image.Image; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import java.util.List; @SpringBootTest public class ImageDaoTest { @Autowired ImageDao imageDao; @Test public void testSelectAll() { long topicId = 1585529145054986240L; List imageList = imageDao.selectListOrderById(topicId); System.out.println(imageList); System.out.println(imageList.size()); } } ================================================ FILE: acimage_image/src/test/java/com/acimage/image/dao/ImageHashDaoDaoTest.java ================================================ package com.acimage.image.dao; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest public class ImageHashDaoDaoTest { @Autowired ImageHashDao imageHashDao; @Test public void testSelectAll(){ System.out.println(imageHashDao.selectList(null)); } } ================================================ FILE: acimage_image/src/test/java/com/acimage/image/service/FileServiceTest.java ================================================ package com.acimage.image.service; import com.acimage.image.service.imagehash.SearchImageService; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.mock.web.MockMultipartFile; import org.springframework.web.multipart.MultipartFile; import java.io.File; import java.io.FileInputStream; import java.io.IOException; @SpringBootTest public class FileServiceTest { @Autowired SearchImageService searchImageService; @Test void searchMostSimilarImagesTest(){ File file=new File("F:\\MyImage\\素材\\爱丽丝.jpeg"); file=new File("F:\\MyImage\\素材\\大忍.jpg"); MultipartFile cMultiFile = null; try { cMultiFile = new MockMultipartFile("file", file.getName(), null, new FileInputStream(file)); } catch (IOException e) { throw new RuntimeException(e); } System.out.println(searchImageService.searchMostSimilarImages(cMultiFile)); } } ================================================ FILE: acimage_image/src/test/java/com/acimage/image/service/SearchImageServiceTest.java ================================================ package com.acimage.image.service; import com.acimage.common.utils.ExceptionUtils; import com.acimage.image.service.imagehash.SearchImageService; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.InputStream; @SpringBootTest public class SearchImageServiceTest { @Autowired SearchImageService searchImageService; @Test void processImagesHashForNotProcessedImagesTest(){ InputStream inputStream=null; try { inputStream = new FileInputStream("F:\\MyImage\\素材\\0001.jpeg"); } catch (FileNotFoundException e) { ExceptionUtils.printIfDev(e); throw new RuntimeException(e); } searchImageService.hashImageByDhashAlgorithm(inputStream,1L); try { Thread.sleep(4000); } catch (InterruptedException e) { throw new RuntimeException(e); } } } ================================================ FILE: acimage_image/src/test/java/com/acimage/image/utils/ImageHashDaoUtilsTest.java ================================================ package com.acimage.image.utils; import net.coobird.thumbnailator.Thumbnails; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import javax.imageio.ImageIO; import java.awt.*; import java.awt.image.BufferedImage; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.List; @SpringBootTest public class ImageHashDaoUtilsTest { @Test public void scaleImageTest(){ BufferedImage inputStream= null; try { inputStream = ImageIO.read(new FileInputStream("F:\\MyImage\\素材\\bg2.png")); // inputStream = new FileInputStream("F:\\MyImage\\素材\\1.jpeg"); List inputStreams=new ArrayList<>(List.of(inputStream)); Thumbnails.Builder inputStreamBuilder=Thumbnails.fromImages(inputStreams); inputStreamBuilder.forceSize(9,8); // inputStreamBuilder.asBufferedImage().toFile(new File("F:\\MyImage\\素材\\0001.jpeg")); ImageIO.write(inputStreamBuilder.asBufferedImage(),"jpeg",new File("F:\\MyImage\\素材\\0bg2.png")); // inputStreamBuilder.asFiles(new ArrayList<>(List.of(new File("F:\\MyImage\\素材\\0001.jpeg")))); } catch (IOException e) { throw new RuntimeException(e); } } @Test public void getImageDhashFromTest(){ InputStream inputStream1= null; InputStream inputStream2= null; try { inputStream1 = new FileInputStream("F:\\MyImage\\素材\\1.jpeg"); inputStream2 = new FileInputStream("F:\\MyImage\\素材\\0001.jpeg"); System.out.println(DhashUtils.distanceBetween(inputStream1,inputStream2)); } catch (IOException e) { throw new RuntimeException(e); } } @Test public void imageDistanceTest(){ Image img=Toolkit.getDefaultToolkit().getImage(System.getProperty("user.dir")); InputStream inputStream1= null; InputStream inputStream2= null; try { inputStream1 = new FileInputStream("F:\\MyImage\\素材\\0001.jpeg"); inputStream2 = new FileInputStream("F:\\MyImage\\素材\\1.jpeg"); System.out.println(DhashUtils.distanceBetween(inputStream1,inputStream2)); } catch (IOException e) { throw new RuntimeException(e); } } @Test public void bitUtilsTest(){ long a=185L; System.out.println(BitUtils.longToStr64(a)); System.out.println(BitUtils.str64ToLong(BitUtils.longToStr64(a))); } } ================================================ FILE: acimage_user/pom.xml ================================================ acimage com.acimage 0.0.1-SNAPSHOT 4.0.0 acimage_user jar acimage_user http://maven.apache.org UTF-8 com.acimage acimage_common 0.0.1-SNAPSHOT com.acimage acimage_feign 0.0.1-SNAPSHOT org.springframework.boot spring-boot-starter-aop org.springframework.boot spring-boot-starter-websocket org.springframework.boot spring-boot-starter-mail com.alibaba druid mysql mysql-connector-java runtime org.springframework.boot spring-boot-starter-amqp io.github.toolgood toolgood-words com.github.ben-manes.caffeine caffeine org.springframework.boot spring-boot-maven-plugin true org.apache.maven.plugins maven-surefire-plugin true ================================================ FILE: acimage_user/src/main/java/com/acimage/user/UserApplication.java ================================================ package com.acimage.user; import lombok.extern.slf4j.Slf4j; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.openfeign.EnableFeignClients; import org.springframework.context.annotation.ComponentScan; import org.springframework.scheduling.annotation.EnableScheduling; @Slf4j @SpringBootApplication @EnableDiscoveryClient @EnableScheduling @EnableFeignClients(basePackages="com.acimage.feign") @MapperScan("com.acimage.user.dao") @ComponentScan(value={"com.acimage"}) public class UserApplication { public static void main(String[] args) { SpringApplication.run(UserApplication.class, args); log.info("------------->>>User中心启动<<<-------------"); } } ================================================ FILE: acimage_user/src/main/java/com/acimage/user/dao/CommentMsgDao.java ================================================ package com.acimage.user.dao; import com.acimage.common.model.domain.user.CommentMsg; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Param; import java.util.List; public interface CommentMsgDao extends BaseMapper { List selectCommentMsgsWithUser(@Param("toUserId") long toUserId,@Param("startIndex") int startIndex,@Param("size") int size); } ================================================ FILE: acimage_user/src/main/java/com/acimage/user/dao/UserDao.java ================================================ package com.acimage.user.dao; import com.acimage.common.model.domain.user.User; import com.baomidou.mybatisplus.core.mapper.BaseMapper; public interface UserDao extends BaseMapper { // @Insert("insert into tb_user(id,username,pwd,email,salt) values (#{id},#{username},#{password},#{email},#{salt})") // void insert(@Param("id") long id,@Param("username") String username,@Param("password") String passwordDigest, // @Param("email") String email,@Param("salt") String salt); // UserPrivacy selectUserInfoWithTopicCountStarCountById(@Param("id") long id); // @Insert("insert into tb_user(id,username,pwd,email,salt) values (#{id},#{username},#{password},#{email},#{salt})") // void insert(@Param("id") long id,@Param("username") String username,@Param("password") String passwordDigest, // @Param("email") String email,@Param("salt") String salt); // // @Update("update tb_user set username=#{username} where id=#{id}") // Integer updateUsername(@Param("id") long id, @Param("username") String username); // // // @Select("select * from tb_user where username=#{username}") // UserPrivacy selectUserInfoByUsername(@Param("username") String username); // // @Select("select * from tb_user where email=#{email}") // UserPrivacy selectUserInfoByEmail(@Param("email") String email); // // UserPrivacy selectUserInfoWithTopicCountStarCountById(@Param("id") long id); } ================================================ FILE: acimage_user/src/main/java/com/acimage/user/dao/UserMsgDao.java ================================================ package com.acimage.user.dao; import com.acimage.common.model.domain.user.UserMsg; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Update; public interface UserMsgDao extends BaseMapper { @Update("update tb_user_msg set ${column}=${column}+#{increment} where user_id=#{userId}") void increaseColumn(@Param("userId") long userId,@Param("column") String column,@Param("increment") int increment); } ================================================ FILE: acimage_user/src/main/java/com/acimage/user/dao/UserPrivacyDao.java ================================================ package com.acimage.user.dao; import com.acimage.common.model.domain.user.UserPrivacy; import com.baomidou.mybatisplus.core.mapper.BaseMapper; public interface UserPrivacyDao extends BaseMapper { // @Insert("insert into tb_user(id,username,pwd,email,salt) values (#{id},#{username},#{password},#{email},#{salt})") // void insert(@Param("id") long id,@Param("username") String username,@Param("password") String passwordDigest, // @Param("email") String email,@Param("salt") String salt); // @Update("update tb_user set username=#{username} where id=#{id}") // Integer updateUsername(@Param("id") long id, @Param("username") String username); } ================================================ FILE: acimage_user/src/main/java/com/acimage/user/global/config/WebMvcConfig.java ================================================ package com.acimage.user.global.config; import com.acimage.common.web.interceptor.JwtInterceptor; import com.acimage.common.web.interceptor.AccessInterceptor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @Slf4j @Configuration public class WebMvcConfig implements WebMvcConfigurer { @Autowired JwtInterceptor jwtInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { List commonExcludePathPatterns = new ArrayList<>( Arrays.asList( "/", "/error")); registry.addInterceptor(jwtInterceptor).addPathPatterns("/**") .excludePathPatterns(commonExcludePathPatterns).order(20); registry.addInterceptor(new AccessInterceptor()).addPathPatterns("/**") .excludePathPatterns(commonExcludePathPatterns).order(30); } } ================================================ FILE: acimage_user/src/main/java/com/acimage/user/global/config/WebSocketConfig.java ================================================ package com.acimage.user.global.config; import com.acimage.user.web.websocket.MyWebSocketHandler; import com.acimage.user.web.websocket.MyHandshakeInterceptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.socket.config.annotation.*; import org.springframework.web.socket.server.standard.ServerEndpointExporter; @Configuration @EnableWebSocket //@EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketConfigurer, WebSocketMessageBrokerConfigurer { @Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } @Autowired MyHandshakeInterceptor interceptor; @Autowired MyWebSocketHandler myWebSocketHandler; @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(myWebSocketHandler, "/websocket") .setAllowedOrigins("*") .addInterceptors(interceptor); } // @Override // public void registerStompEndpoints(StompEndpointRegistry registry) { // System.out.println("注册阿萨德老好saddddd看"); // registry.addEndpoint("/wsx.ws") // .addInterceptors(interceptor)//拦截器方式1,暂不用 // .setAllowedOrigins("*");//开启socketJs // } } ================================================ FILE: acimage_user/src/main/java/com/acimage/user/global/consts/PageSizeConsts.java ================================================ package com.acimage.user.global.consts; public class PageSizeConsts { public static final int TOPIC_COMMENTS =5; public static final int ACTIVITY_TOPICS =5; public static final int ACTIVITY_COMMENTS =5; public static final int ACTIVITY_STARS =5; public static final int FORUM_TOPICS =10; } ================================================ FILE: acimage_user/src/main/java/com/acimage/user/global/consts/StorePrefixConst.java ================================================ package com.acimage.user.global.consts; public class StorePrefixConst { public final static String TOPIC_IMAGE="topicImage"; public final static String USER_PHOTO="userPhoto"; } ================================================ FILE: acimage_user/src/main/java/com/acimage/user/global/consts/WebSocketSessionConstants.java ================================================ package com.acimage.user.global.consts; public class WebSocketSessionConstants { public static final String KEY_USER_ID="userId"; } ================================================ FILE: acimage_user/src/main/java/com/acimage/user/model/request/UserLoginReq.java ================================================ package com.acimage.user.model.request; import lombok.Data; import lombok.NoArgsConstructor; import javax.validation.constraints.Email; import javax.validation.constraints.Size; @Data @NoArgsConstructor public class UserLoginReq { @Email(message = "邮箱格式错误") @Size(min=6,max=32,message = "邮箱长度在6到32之间") private String email; @Size(min = 100, max = 2000, message = "非法密码") String password; @Size(min=4,max=6,message = "验证码长度不符") String verifyCode; } ================================================ FILE: acimage_user/src/main/java/com/acimage/user/model/request/UserRegisterReq.java ================================================ package com.acimage.user.model.request; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import javax.validation.constraints.*; @Data @NoArgsConstructor @AllArgsConstructor public class UserRegisterReq { public static final int VERIFY_CODE_LENGTH=8; @Size(min=2,max=12,message = "用户名长度在2到12之间") private String username; @Size(min=100,max=2000,message = "非法密码") private String password; @Email(message = "邮箱格式错误") @Size(min=6,max=32,message = "邮箱长度在6到32之间") private String email; /** * 有可能带空格 */ @Size(min=VERIFY_CODE_LENGTH,max = VERIFY_CODE_LENGTH+2) private String verifyCode; } ================================================ FILE: acimage_user/src/main/java/com/acimage/user/model/vo/ProfileVo.java ================================================ package com.acimage.user.model.vo; import com.fasterxml.jackson.annotation.JsonFormat; import lombok.Data; import java.util.Date; @Data public class ProfileVo { String username; String email; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") Date registerTime; Integer topicCount; Integer starCount; } ================================================ FILE: acimage_user/src/main/java/com/acimage/user/mq/config/SyncUserMqConfig.java ================================================ package com.acimage.user.mq.config; import com.acimage.common.global.consts.MqConstants; import org.springframework.amqp.core.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class SyncUserMqConfig { @Autowired AmqpAdmin rabbitAdmin; @Bean public Queue syncUserQueue() { // durable:是否持久化,默认是false,持久化队列:会被存储在磁盘上,当消息代理重启时仍然存在,暂存队列:当前连接有效 // exclusive:默认也是false,只能被当前创建的连接使用,而且当连接关闭后队列即被删除。此参考优先级高于durable // autoDelete:是否自动删除,当没有生产者或者消费者使用此队列,该队列会自动删除。 // return new Queue("TestDirectQueue",true,true,false); //一般设置一下队列的持久化就好,其余两个就是默认false return new Queue(MqConstants.SYNC_USER_QUEUE, true); } @Bean DirectExchange syncUserExchange() { return new DirectExchange(MqConstants.COMMUNITY_USER_EXCHANGE, true, false); } //绑定 将队列和交换机绑定, 并设置用于匹配键 @Bean Binding syncUserBinding() { return BindingBuilder.bind(syncUserQueue()).to(syncUserExchange()).with(MqConstants.SYNC_USER_ROUTE); } //创建交换机和对列 @Bean public void createExchangeQueueForSyncUser() { rabbitAdmin.declareExchange(syncUserExchange()); rabbitAdmin.declareQueue(syncUserQueue()); } } ================================================ FILE: acimage_user/src/main/java/com/acimage/user/mq/consumer/UserMsgConsumer.java ================================================ package com.acimage.user.mq.consumer; import com.acimage.common.global.consts.MqConstants; import com.acimage.common.model.domain.user.CommentMsg; import com.acimage.common.model.domain.user.UserMsg; import com.acimage.common.model.mq.dto.EsAddDto; import com.acimage.common.model.mq.dto.EsDeleteDto; import com.acimage.common.model.mq.dto.EsUpdateByIdDto; import com.acimage.common.utils.EsUtils; import com.acimage.common.utils.ExceptionUtils; import com.acimage.user.service.commentmsg.CommentMsgWriteService; import com.acimage.user.service.usermsg.UserMsgQueryService; import com.acimage.user.service.usermsg.UserMsgWriteService; import com.acimage.user.web.websocket.MyWebSocketHandler; import com.rabbitmq.client.Channel; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.core.Message; import org.springframework.amqp.rabbit.annotation.RabbitHandler; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.io.IOException; @Slf4j @Component @RabbitListener(queues = MqConstants.USER_MSG_QUEUE) public class UserMsgConsumer { @Autowired UserMsgWriteService userMsgWriteService; @Autowired CommentMsgWriteService commentMsgWriteService; @Autowired MyWebSocketHandler webSocketHandler; @RabbitHandler public void commentMsg(Channel channel, Message message, CommentMsg commentMsg) { log.info("用户评论消息通知:{}", commentMsg); try { //插入用户消息 commentMsgWriteService.insert(commentMsg); //评论消息+1 userMsgWriteService.increaseMsg(commentMsg.getToUserId(), UserMsg::getCommentMsgCount, 1); //推送消息给用户 webSocketHandler.sendMsgCount(commentMsg.getToUserId()); } catch (Exception e) { ExceptionUtils.printIfDev(e); log.error("用户评论消息通知失败 error:{} data:{}", e.getMessage(), commentMsg); } finally { String messageBody = new String(message.getBody()); try { channel.basicAck(message.getMessageProperties().getDeliveryTag(), false); } catch (IOException e) { ExceptionUtils.printIfDev(e); log.error("用户评论消息通知失败 error:{} message:{}", e.getMessage(), messageBody); try { channel.basicReject(message.getMessageProperties().getDeliveryTag(), false); } catch (IOException ex) { ExceptionUtils.printIfDev(ex); log.error("用户评论消息通知reject失败 error:{} message:{}", ex.getMessage(), messageBody); } } } } } ================================================ FILE: acimage_user/src/main/java/com/acimage/user/mq/producer/SyncUserMqProducer.java ================================================ package com.acimage.user.mq.producer; import com.acimage.common.global.consts.MqConstants; import com.acimage.common.model.domain.community.CmtyUser; import com.acimage.common.model.mq.dto.UserIdWithPhotoUrl; import com.acimage.common.model.mq.dto.UserIdWithUsername; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component public class SyncUserMqProducer { @Autowired RabbitTemplate rabbitTemplate; public void sendSyncUsernameMessage(UserIdWithUsername userIdWithUsername){ rabbitTemplate.convertAndSend(MqConstants.COMMUNITY_USER_EXCHANGE, MqConstants.SYNC_USER_ROUTE,userIdWithUsername); } public void sendSyncUserPhotoUrlMessage(UserIdWithPhotoUrl userIdWithPhotoUrl){ rabbitTemplate.convertAndSend(MqConstants.COMMUNITY_USER_EXCHANGE, MqConstants.SYNC_USER_ROUTE,userIdWithPhotoUrl); } public void sendAddUserMessage(CmtyUser cmtyUser){ rabbitTemplate.convertAndSend(MqConstants.COMMUNITY_USER_EXCHANGE, MqConstants.SYNC_USER_ROUTE, cmtyUser); } } ================================================ FILE: acimage_user/src/main/java/com/acimage/user/service/commentmsg/CommentMsgQueryService.java ================================================ package com.acimage.user.service.commentmsg; import com.acimage.common.model.domain.user.CommentMsg; import com.acimage.common.model.page.MyPage; public interface CommentMsgQueryService { MyPage pageMyCommentMsg(long userId, int pageNo, int pageSize); } ================================================ FILE: acimage_user/src/main/java/com/acimage/user/service/commentmsg/CommentMsgWriteService.java ================================================ package com.acimage.user.service.commentmsg; import com.acimage.common.model.domain.user.CommentMsg; public interface CommentMsgWriteService { void insert(CommentMsg commentMsg); } ================================================ FILE: acimage_user/src/main/java/com/acimage/user/service/commentmsg/impl/CommentMsgQueryServiceImpl.java ================================================ package com.acimage.user.service.commentmsg.impl; import com.acimage.common.model.domain.user.CommentMsg; import com.acimage.common.model.page.MyPage; import com.acimage.common.utils.common.PageUtils; import com.acimage.user.dao.CommentMsgDao; import com.acimage.user.service.commentmsg.CommentMsgQueryService; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; @Service public class CommentMsgQueryServiceImpl implements CommentMsgQueryService { @Autowired CommentMsgDao commentMsgDao; @Override public MyPage pageMyCommentMsg(long userId, int pageNo, int pageSize) { LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); qw.eq(CommentMsg::getToUserId, userId); int startIndex = PageUtils.startIndexOf(pageNo, pageSize); List commentMsgList = commentMsgDao.selectCommentMsgsWithUser(userId, startIndex, pageSize); int totalCount = commentMsgDao.selectCount(qw); return new MyPage<>(totalCount, commentMsgList); } } ================================================ FILE: acimage_user/src/main/java/com/acimage/user/service/commentmsg/impl/CommentMsgWriteServiceImpl.java ================================================ package com.acimage.user.service.commentmsg.impl; import com.acimage.common.model.domain.user.CommentMsg; import com.acimage.user.dao.CommentMsgDao; import com.acimage.user.service.commentmsg.CommentMsgWriteService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class CommentMsgWriteServiceImpl implements CommentMsgWriteService { @Autowired CommentMsgDao commentMsgDao; @Override public void insert(CommentMsg commentMsg){ commentMsgDao.insert(commentMsg); } } ================================================ FILE: acimage_user/src/main/java/com/acimage/user/service/mail/MainService.java ================================================ package com.acimage.user.service.mail; import java.util.concurrent.TimeUnit; public interface MainService { void sendTextMailMessage(String to, String subject, String text); void sendVerifyCodeMailMessage(String to, String code, int timeoutMinute); } ================================================ FILE: acimage_user/src/main/java/com/acimage/user/service/mail/impl/MailServiceImpl.java ================================================ package com.acimage.user.service.mail.impl; import com.acimage.user.service.mail.MainService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.mail.javamail.MimeMessageHelper; import org.springframework.stereotype.Service; import javax.mail.MessagingException; import java.util.Date; @Service @Slf4j public class MailServiceImpl implements MainService { /** * 注入邮件工具类 */ @Autowired JavaMailSender javaMailSender; @Value("${spring.mail.username}") String sendMailer; /** * 发送纯文本邮件 */ @Override public void sendTextMailMessage(String to, String subject, String text) { try { //true 代表支持复杂的类型 MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(javaMailSender.createMimeMessage(), true); //邮件发信人 mimeMessageHelper.setFrom(sendMailer); //邮件收信人 mimeMessageHelper.setTo(to); //邮件主题 mimeMessageHelper.setSubject(subject); //邮件内容 mimeMessageHelper.setText(text); //邮件发送时间 mimeMessageHelper.setSentDate(new Date()); //发送邮件 javaMailSender.send(mimeMessageHelper.getMimeMessage()); log.info("发送邮件成功:{}->{} subject;{} text;{}", sendMailer, to, subject, text); } catch (MessagingException e) { log.error("发送邮件失败:{}->{} subject;{} text;{} error:{}", sendMailer, to, subject, text, e.getMessage()); } } /** * 发送html邮件 */ @Override public void sendVerifyCodeMailMessage(String to, String code,int timeoutMinute) { String content = "acimage网站正在验证邮箱
" + "验证码:" + code+"
"+ "有效时间"+timeoutMinute+"分钟"; try { //true 代表支持复杂的类型 MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(javaMailSender.createMimeMessage(), true); //邮件发信人 mimeMessageHelper.setFrom(sendMailer); //邮件收信人 1或多个 mimeMessageHelper.setTo(to); //邮件主题 mimeMessageHelper.setSubject("acimage网站验证邮箱"); //邮件内容 true 代表支持html mimeMessageHelper.setText(content, true); //邮件发送时间 mimeMessageHelper.setSentDate(new Date()); //发送邮件 javaMailSender.send(mimeMessageHelper.getMimeMessage()); log.info("发送验证码邮件成功:to:{} code;{} ", to, code); } catch (MessagingException e) { log.error("发送验证码邮件失败:to:{} code;{} error:{}", to, code,e.getMessage()); } } } ================================================ FILE: acimage_user/src/main/java/com/acimage/user/service/user/LoginService.java ================================================ package com.acimage.user.service.user; import com.acimage.user.model.request.UserLoginReq; import com.acimage.user.model.request.UserRegisterReq; import org.springframework.transaction.annotation.Transactional; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; public interface LoginService { String getPublicKey(); @Transactional String registerUser(UserRegisterReq userRegister); @Transactional String login(UserLoginReq userLogin); void logout(HttpServletRequest request); void checkAndSendCodeToEmail(String email); } ================================================ FILE: acimage_user/src/main/java/com/acimage/user/service/user/UserInfoService.java ================================================ package com.acimage.user.service.user; import com.acimage.common.model.domain.user.User; import com.acimage.user.model.vo.ProfileVo; public interface UserInfoService { ProfileVo getProfile(); } ================================================ FILE: acimage_user/src/main/java/com/acimage/user/service/user/UserQueryService.java ================================================ package com.acimage.user.service.user; import com.acimage.common.model.domain.user.User; public interface UserQueryService { User getUser(long userId); Boolean isUsernameExist(String username); } ================================================ FILE: acimage_user/src/main/java/com/acimage/user/service/user/UserRankService.java ================================================ package com.acimage.user.service.user; import com.acimage.common.model.domain.user.User; import java.util.List; public interface UserRankService { List pageUsersRankByStarCount(); } ================================================ FILE: acimage_user/src/main/java/com/acimage/user/service/user/UserWriteService.java ================================================ package com.acimage.user.service.user; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; import javax.servlet.http.HttpServletResponse; public interface UserWriteService { @Transactional String updateUsername(String username); @Deprecated @Transactional(rollbackFor = Exception.class) String uploadPhotoAndUpdatePhotoUrl(MultipartFile photoFile); String updatePhotoUrl(String newPhotoUrl); } ================================================ FILE: acimage_user/src/main/java/com/acimage/user/service/user/consts/KeyConstants.java ================================================ package com.acimage.user.service.user.consts; public class KeyConstants { public static final String STRINGKP_USER = "acimage:users:id:"; public static final String STRINGKP_USERNAME = "acimage:users:username:"; } ================================================ FILE: acimage_user/src/main/java/com/acimage/user/service/user/impl/LoginServiceImpl.java ================================================ package com.acimage.user.service.user.impl; import cn.hutool.core.util.IdUtil; import cn.hutool.crypto.digest.DigestUtil; import com.acimage.common.global.consts.JwtConstants; import com.acimage.common.global.context.UserContext; import com.acimage.common.global.exception.BusinessException; import com.acimage.common.global.consts.HeaderKeyConstants; import com.acimage.common.model.domain.community.CmtyUser; import com.acimage.common.model.domain.user.User; import com.acimage.common.service.TokenService; import com.acimage.common.utils.IdGenerator; import com.acimage.common.utils.SensitiveWordUtils; import com.acimage.common.utils.redis.RedisUtils; import com.acimage.user.dao.UserDao; import com.acimage.user.dao.UserPrivacyDao; import com.acimage.common.model.domain.user.UserPrivacy; import com.acimage.user.model.request.UserLoginReq; import com.acimage.user.model.request.UserRegisterReq; import com.acimage.user.mq.producer.SyncUserMqProducer; import com.acimage.user.service.user.LoginService; import com.acimage.common.utils.RsaUtils; import com.acimage.user.service.usermsg.UserMsgWriteService; import com.acimage.user.service.verify.VerifyCodeService; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import javax.servlet.http.HttpServletRequest; import java.util.concurrent.TimeUnit; import java.util.regex.Pattern; @Slf4j @Service public class LoginServiceImpl implements LoginService { public static final String PASSWORD_PATTERN = "^(?![a-zA-Z]+$)[0-9A-Za-z\\W]{6,16}$"; public static final String STRINGKP_SEND_EMAIL = "acimage:user:logins:sendEmailCode:email:"; @Autowired UserDao userDao; @Autowired UserPrivacyDao userPrivacyDao; @Autowired UserMsgWriteService userMsgWriteService; @Autowired TokenService tokenService; @Autowired SyncUserMqProducer syncUserMqProducer; @Autowired VerifyCodeService verifyCodeService; @Autowired RedisUtils redisUtils; @Override public String getPublicKey() { return RsaUtils.getPublicKey(); } @Override public String registerUser(UserRegisterReq userRegister) { //获取私钥 String privateKey = RsaUtils.getPrivateKey(); //解密并校验密码 String passwordDecrypt = RsaUtils.decrypt(privateKey, userRegister.getPassword()); // log.info(" 解密为:{}", passwordDecrypt); if (!Pattern.matches(PASSWORD_PATTERN, passwordDecrypt)) { throw new BusinessException("密码长度为6至16位,且只含数字、字母和特殊字符"); } //判断username是否存在 String username = SensitiveWordUtils.filter(userRegister.getUsername()); LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); qw.eq(User::getUsername, username); User userByUsername = userDao.selectOne(qw); if (userByUsername != null) { log.info("用户:无 注册 错误:用户名{}已存在", username); throw new BusinessException("用户名已存在"); } //判断email是否存在 String email = userRegister.getEmail(); LambdaQueryWrapper qwForEmail = new LambdaQueryWrapper<>(); qwForEmail.eq(UserPrivacy::getEmail, email); UserPrivacy userByEmail = userPrivacyDao.selectOne(qwForEmail); if (userByEmail != null) { log.warn("用户注册 error:邮箱{}已存在", email); throw new BusinessException("email已存在"); } //生成随机盐 String salt = IdUtil.simpleUUID(); log.debug("随机盐为 {}", salt); //利用随机盐进行密钥摘要或加密 String passwordDigest = DigestUtil.md5Hex(salt + passwordDecrypt); log.debug(" md5摘要为:{}", passwordDigest); //生成用户id long userId = IdGenerator.getSnowflakeNextId(); log.debug("雪花算法生成id为:{}", userId); //插入用户 User insertedUser = new User(userId, username); userDao.insert(insertedUser); UserPrivacy userPrivacy = new UserPrivacy(userId, passwordDigest, salt, email); userPrivacyDao.insert(userPrivacy); userMsgWriteService.insert(userId); //返回token String defaultPhotoUrl = ""; //mq发送消息,同步数据 CmtyUser cmtyUser = new CmtyUser(); cmtyUser.setId(userId); cmtyUser.setUsername(username); cmtyUser.setPhotoUrl(defaultPhotoUrl); syncUserMqProducer.sendAddUserMessage(cmtyUser); return tokenService.createAndRecordToken(userId, username, defaultPhotoUrl, JwtConstants.USER_EXPIRE_DAYS); } @Override public String login(UserLoginReq userLogin) { String email = userLogin.getEmail(); String password = userLogin.getPassword(); LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); qw.eq(UserPrivacy::getEmail, email); UserPrivacy userPrivacy = userPrivacyDao.selectOne(qw); if (userPrivacy == null) { throw new BusinessException("邮箱不存在"); } //找到密码 long userId = userPrivacy.getUserId(); String salt = userPrivacy.getSalt(); String passwordDigest = userPrivacy.getPwd(); //获取私钥 String privateKey = RsaUtils.getPrivateKey(); //解密密码 String passwordDecrypt = RsaUtils.decrypt(privateKey, password); // log.info(" 解密为:{}", passwordDecrypt); //判断密码正确性 if (!DigestUtil.md5Hex(salt + passwordDecrypt).equals(passwordDigest)) { log.warn("登录 错误:邮箱{} 或密码错误", email); throw new BusinessException("用户名或密码错误"); } //返回token User user = userDao.selectById(userId); return tokenService.createAndRecordToken(userId, user.getUsername(), user.getPhotoUrl(), JwtConstants.USER_EXPIRE_DAYS); } @Override public void logout(HttpServletRequest request) { //获取cookie中的token String token = request.getHeader(HeaderKeyConstants.AUTHORIZATION); tokenService.invalidate(token); } @Override public void checkAndSendCodeToEmail(String email) { if (redisUtils.getForString(STRINGKP_SEND_EMAIL + email) != null) { log.info("ip:{} 已发送过邮箱验证码了", UserContext.getIp()); throw new BusinessException("email验证码已发送过了,每30s可尝试一次"); } //检查邮箱是否存在 LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); qw.select(UserPrivacy::getEmail).eq(UserPrivacy::getEmail, email); if (userPrivacyDao.selectOne(qw) != null) { throw new BusinessException("邮箱已存在"); } verifyCodeService.sendVerifyCodeToEmail(email, UserRegisterReq.VERIFY_CODE_LENGTH); long timeout = 30; redisUtils.setAsString(STRINGKP_SEND_EMAIL + email, "0", timeout, TimeUnit.SECONDS); } } ================================================ FILE: acimage_user/src/main/java/com/acimage/user/service/user/impl/UserInfoServiceImpl.java ================================================ package com.acimage.user.service.user.impl; import com.acimage.common.global.context.UserContext; import com.acimage.common.model.domain.community.CmtyUser; import com.acimage.common.redis.annotation.QueryRedis; import com.acimage.feign.client.CmtyUserClient; import com.acimage.user.dao.UserDao; import com.acimage.user.dao.UserPrivacyDao; import com.acimage.common.model.domain.user.UserPrivacy; import com.acimage.user.model.vo.ProfileVo; import com.acimage.user.service.user.UserInfoService; import com.acimage.user.service.user.UserQueryService; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.concurrent.TimeUnit; @Service @Slf4j public class UserInfoServiceImpl implements UserInfoService { @Autowired UserQueryService userQueryService; @Autowired UserDao userDao; @Autowired UserPrivacyDao userPrivacyDao; @Autowired CmtyUserClient cmtyUserClient; @QueryRedis(keyPrefix = "acimage:users:profile:userId:", expire = 2L, unit = TimeUnit.SECONDS) @Override public ProfileVo getProfile() { CmtyUser cmtyUser = cmtyUserClient.queryCmtyUser(UserContext.getUserId()).getData(); ProfileVo profileVo = new ProfileVo(); profileVo.setStarCount(cmtyUser.getStarCount()); profileVo.setTopicCount(cmtyUser.getTopicCount()); LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); qw.eq(UserPrivacy::getUserId, UserContext.getUserId()); UserPrivacy userPrivacy = userPrivacyDao.selectOne(qw); profileVo.setEmail(userPrivacy.getEmail()); profileVo.setRegisterTime(userPrivacy.getRegisterTime()); profileVo.setUsername(UserContext.getUsername()); return profileVo; } } ================================================ FILE: acimage_user/src/main/java/com/acimage/user/service/user/impl/UserQueryServiceImpl.java ================================================ package com.acimage.user.service.user.impl; import com.acimage.common.global.exception.BusinessException; import com.acimage.common.global.context.UserContext; import com.acimage.common.model.domain.user.User; import com.acimage.common.redis.annotation.KeyParam; import com.acimage.common.redis.annotation.QueryRedis; import com.acimage.user.dao.UserDao; import com.acimage.user.service.user.UserQueryService; import com.acimage.user.service.user.consts.KeyConstants; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.concurrent.TimeUnit; @Slf4j @Service public class UserQueryServiceImpl implements UserQueryService { @Autowired UserDao userDao; @QueryRedis(keyPrefix = KeyConstants.STRINGKP_USER, expire = 37L) @Override public User getUser(@KeyParam long userId) { User user = userDao.selectById(userId); if (user == null) { log.error("用户:{} 查询 用户{} 错误:用户不存在", UserContext.getUsername(), userId); throw new BusinessException("该用户不存在"); } return user; } @QueryRedis(keyPrefix = KeyConstants.STRINGKP_USERNAME, expire = 17L, unit = TimeUnit.SECONDS) @Override public Boolean isUsernameExist(@KeyParam String username) { LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); qw.eq(User::getUsername, username); return userDao.selectOne(qw) != null; } } ================================================ FILE: acimage_user/src/main/java/com/acimage/user/service/user/impl/UserRankServiceImpl.java ================================================ package com.acimage.user.service.user.impl; import com.acimage.common.model.domain.user.User; import com.acimage.user.service.user.UserRankService; import java.util.List; public class UserRankServiceImpl implements UserRankService { @Override public List pageUsersRankByStarCount() { return null; } } ================================================ FILE: acimage_user/src/main/java/com/acimage/user/service/user/impl/UserWriteServiceImpl.java ================================================ package com.acimage.user.service.user.impl; import cn.hutool.core.util.StrUtil; import com.acimage.common.global.consts.JwtConstants; import com.acimage.common.global.context.UserContext; import com.acimage.common.model.domain.user.User; import com.acimage.common.model.mq.dto.UserIdWithPhotoUrl; import com.acimage.common.model.mq.dto.UserIdWithUsername; import com.acimage.common.utils.SensitiveWordUtils; import com.acimage.common.utils.common.FileUtils; import com.acimage.common.utils.IdGenerator; import com.acimage.common.deprecated.QiniuUtils; import com.acimage.common.service.TokenService; import com.acimage.common.utils.redis.RedisUtils; import com.acimage.user.dao.UserDao; import com.acimage.user.global.consts.StorePrefixConst; import com.acimage.user.mq.producer.SyncUserMqProducer; import com.acimage.user.service.user.UserWriteService; import com.acimage.user.service.user.consts.KeyConstants; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; import javax.servlet.http.HttpServletResponse; import java.util.Date; @Slf4j @Service public class UserWriteServiceImpl implements UserWriteService { @Autowired UserDao userDao; @Autowired TokenService tokenService; @Autowired RedisUtils redisUtils; @Autowired(required = false) QiniuUtils qiniuUtils; @Autowired SyncUserMqProducer syncUserMqProducer; @Override public String updateUsername(String username) { String filterUsername=SensitiveWordUtils.filter(username); LambdaUpdateWrapper qw = new LambdaUpdateWrapper<>(); qw.eq(User::getId, UserContext.getUserId()) .set(User::getUsername, filterUsername); userDao.update(null, qw); String token = tokenService.createAndRecordToken(UserContext.getUserId(), filterUsername, UserContext.getPhotoUrl(), JwtConstants.USER_EXPIRE_DAYS); //删除redis数据 String key = KeyConstants.STRINGKP_USER + UserContext.getUserId(); redisUtils.delete(key); //mq发送同步用户名消息 syncUserMqProducer.sendSyncUsernameMessage(new UserIdWithUsername(UserContext.getUserId(),filterUsername)); return token; } @Override public String uploadPhotoAndUpdatePhotoUrl(MultipartFile photoFile) { //上传头像到七牛云 Date now = new Date(); String format = FileUtils.formatOf(photoFile.getOriginalFilename()); String suffix = String.format("%s.%s", IdGenerator.getSnowflakeNextId(), format); String newPhotoUrl = qiniuUtils.generateUrl(suffix, now, StorePrefixConst.USER_PHOTO); //更新photoUrl LambdaUpdateWrapper qw = new LambdaUpdateWrapper<>(); qw.set(User::getPhotoUrl, newPhotoUrl) .eq(User::getId, UserContext.getUserId()); userDao.update(null, qw); qiniuUtils.upload(photoFile, newPhotoUrl); //删除旧头像 if (!StrUtil.isEmpty(UserContext.getPhotoUrl())) { qiniuUtils.deleteFile(UserContext.getPhotoUrl()); } //返回新凭证 String token = tokenService.createAndRecordToken(UserContext.getUserId(), UserContext.getUsername(), newPhotoUrl, JwtConstants.USER_EXPIRE_DAYS); //删除redis数据 String key = KeyConstants.STRINGKP_USER + UserContext.getUserId(); redisUtils.delete(key); return token; } @Override public String updatePhotoUrl(String newPhotoUrl) { //更新photoUrl LambdaUpdateWrapper qw = new LambdaUpdateWrapper<>(); qw.set(User::getPhotoUrl, newPhotoUrl) .eq(User::getId, UserContext.getUserId()); userDao.update(null, qw); //删除缓存的数据 String key = KeyConstants.STRINGKP_USER + UserContext.getUserId(); redisUtils.delete(key); //mq发送同步用户头像消息 syncUserMqProducer.sendSyncUserPhotoUrlMessage(new UserIdWithPhotoUrl(UserContext.getUserId(),newPhotoUrl)); //返回新token return tokenService.createAndRecordToken(UserContext.getUserId(), UserContext.getUsername(), newPhotoUrl, JwtConstants.USER_EXPIRE_DAYS); } } ================================================ FILE: acimage_user/src/main/java/com/acimage/user/service/usermsg/UserMsgQueryService.java ================================================ package com.acimage.user.service.usermsg; import com.acimage.common.model.domain.user.UserMsg; import com.baomidou.mybatisplus.core.toolkit.support.SFunction; public interface UserMsgQueryService { Integer getMsgCount(long userId); } ================================================ FILE: acimage_user/src/main/java/com/acimage/user/service/usermsg/UserMsgWriteService.java ================================================ package com.acimage.user.service.usermsg; import com.acimage.common.model.domain.user.UserMsg; import com.baomidou.mybatisplus.core.toolkit.support.SFunction; public interface UserMsgWriteService { void insert(long userId); void increaseMsg(long userId, SFunction column, int increment); void resetMsgCount(long userId, SFunction column); } ================================================ FILE: acimage_user/src/main/java/com/acimage/user/service/usermsg/impl/UserMsgQueryServiceImpl.java ================================================ package com.acimage.user.service.usermsg.impl; import com.acimage.common.model.domain.user.UserMsg; import com.acimage.user.dao.UserMsgDao; import com.acimage.user.service.usermsg.UserMsgQueryService; import com.acimage.user.web.websocket.MyWebSocketHandler; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.web.bind.annotation.RequestMapping; @Service public class UserMsgQueryServiceImpl implements UserMsgQueryService { @Autowired UserMsgDao userMsgDao; @Override public Integer getMsgCount(long userId) { UserMsg userMsg = userMsgDao.selectById(userId); if (userMsg == null) { return null; } int commentMsgCount = userMsg.getCommentMsgCount(); int starMsgCount = userMsg.getStarMsgCount(); return commentMsgCount + starMsgCount; } } ================================================ FILE: acimage_user/src/main/java/com/acimage/user/service/usermsg/impl/UserMsgWriteServiceImpl.java ================================================ package com.acimage.user.service.usermsg.impl; import com.acimage.common.model.domain.user.UserMsg; import com.acimage.common.utils.LambdaUtils; import com.acimage.user.dao.UserMsgDao; import com.acimage.user.service.usermsg.UserMsgWriteService; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.core.toolkit.support.SFunction; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class UserMsgWriteServiceImpl implements UserMsgWriteService { @Autowired UserMsgDao userMsgDao; @Override public void insert(long userId) { UserMsg userMsg = new UserMsg(); userMsg.setUserId(userId); userMsgDao.insert(userMsg); } @Override public void increaseMsg(long userId, SFunction column, int increment) { String underlineColumn = LambdaUtils.underlineColumnNameOf(column); userMsgDao.increaseColumn(userId, underlineColumn, increment); } @Override public void resetMsgCount(long userId, SFunction column) { LambdaUpdateWrapper uw = new LambdaUpdateWrapper<>(); uw.eq(UserMsg::getUserId, userId) .set(UserMsg::getCommentMsgCount, 0); userMsgDao.update(null, uw); } } ================================================ FILE: acimage_user/src/main/java/com/acimage/user/service/verify/VerifyCodeService.java ================================================ package com.acimage.user.service.verify; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; public interface VerifyCodeService { void writeCodeImageToResponseAndRecord(HttpServletRequest request, HttpServletResponse response); boolean verifyAndRemoveIfSuccess(HttpServletRequest request, String code); void sendVerifyCodeToEmail(String email,int length); boolean verifyEmailByVerifyCode(String email, String code); } ================================================ FILE: acimage_user/src/main/java/com/acimage/user/service/verify/impl/VerifyCodeServiceImpl.java ================================================ package com.acimage.user.service.verify.impl; import cn.hutool.captcha.CaptchaUtil; import cn.hutool.captcha.ShearCaptcha; import cn.hutool.core.util.RandomUtil; import com.acimage.common.utils.redis.RedisUtils; import com.acimage.user.service.mail.MainService; import com.acimage.user.service.verify.VerifyCodeService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.awt.*; import java.io.IOException; import java.util.concurrent.TimeUnit; @Slf4j @Service public class VerifyCodeServiceImpl implements VerifyCodeService { public static final String STRINGKP_VERIFY_CODE = "acimage:user:verifyCode:sessionId:"; public static final String STRINGKP_EMAIL_VERIFY = "acimage:user:logins:verify:email"; @Autowired RedisUtils redisUtils; @Autowired MainService mainService; @Override public void writeCodeImageToResponseAndRecord(HttpServletRequest request, HttpServletResponse response) { int width = 100; int height = 40; int codeCount = 4; int thickness = 4; /** * 注意验证码需要系统有默认字体,如果使用docker alpine,会因为没有默认字体而出错 */ ShearCaptcha captcha = CaptchaUtil.createShearCaptcha(width, height, codeCount, thickness); //图形验证码写出,可以写出到文件,也可以写出到流 ServletOutputStream outputStream=null; try { outputStream = response.getOutputStream(); captcha.write(outputStream); } catch (IOException e) { log.error(e.getMessage()); } //获取验证码中的文字内容 String verifyCode = captcha.getCode(); //记录到redis String sessionId = request.getSession().getId(); long timeout = 30L; redisUtils.setAsString(STRINGKP_VERIFY_CODE + sessionId, verifyCode, timeout, TimeUnit.SECONDS); } @Override public boolean verifyAndRemoveIfSuccess(HttpServletRequest request, String code) { String key = STRINGKP_VERIFY_CODE + request.getSession().getId(); String trueCode = redisUtils.getForString(key); if (trueCode == null) { return false; } if (trueCode.equals(code)) { redisUtils.delete(key); return true; } else { return false; } } @Override public void sendVerifyCodeToEmail(String email, int length) { String code = RandomUtil.randomString(length); int timeoutMinute = 3; mainService.sendVerifyCodeMailMessage(email, code, timeoutMinute); String key = STRINGKP_EMAIL_VERIFY + email; redisUtils.setAsString(key, code, timeoutMinute, TimeUnit.MINUTES); } @Override public boolean verifyEmailByVerifyCode(String email, String code) { String key = STRINGKP_EMAIL_VERIFY + email; String trueCode = redisUtils.getForString(key); if (trueCode == null) { return false; } if (trueCode.equals(code)) { redisUtils.delete(key); return true; } else { return false; } } } ================================================ FILE: acimage_user/src/main/java/com/acimage/user/web/controller/LoginController.java ================================================ package com.acimage.user.web.controller; import com.acimage.common.global.context.UserContext; import com.acimage.common.redis.annotation.RequestLimit; import com.acimage.common.redis.enums.LimitTarget; import com.acimage.common.result.Result; import com.acimage.user.model.request.UserLoginReq; import com.acimage.user.model.request.UserRegisterReq; import com.acimage.user.service.user.LoginService; import com.acimage.user.service.user.UserQueryService; import com.acimage.user.service.verify.VerifyCodeService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import javax.servlet.http.HttpServletRequest; import javax.validation.constraints.Email; import javax.validation.constraints.Size; @Slf4j @RestController @Validated @RequestMapping("/api/user/logins") public class LoginController { @Autowired VerifyCodeService verifyCodeService; @Autowired LoginService loginService; @Autowired UserQueryService userQueryService; @RequestLimit(limitTimes = {1}, durations = {1}, penaltyTimes = {-1}, targets = {LimitTarget.IP}) @GetMapping("/isExist/{username}") public Result isUsernameExist(@Size(min = 2, max = 12, message = "用户名长度在2到12之间") @PathVariable String username) { return Result.ok(userQueryService.isUsernameExist(username)); } @GetMapping("/getPublicKey") public Result getPublicKey() { return Result.ok(loginService.getPublicKey()); } @RequestLimit(limitTimes = {1,5}, durations = {2,1}, penaltyTimes = {-1,-1}, targets = {LimitTarget.IP,LimitTarget.ALL}) @PostMapping("/sendCode") public Result sendVerifyCodeToEmail(@Email @Size(min=6,max=32,message = "邮箱长度在6到32之间") @RequestParam("email") String email, HttpServletRequest request) { loginService.checkAndSendCodeToEmail(email); return Result.ok(); } @RequestLimit(limitTimes = {1}, durations = {2}, penaltyTimes = {-1}, targets = {LimitTarget.IP}) @PostMapping("/doRegister") public Result register(@Validated @RequestBody UserRegisterReq userRegister) { String username=userRegister.getUsername().trim(); String code=userRegister.getVerifyCode().trim(); String email= userRegister.getEmail().trim(); userRegister.setVerifyCode(code); userRegister.setEmail(email); userRegister.setUsername(username); if(username.length()<2){ return Result.fail("用户名有效长度不能少于2"); } if(code.length()!=UserRegisterReq.VERIFY_CODE_LENGTH){ return Result.fail("邮箱验证码错误"); } boolean verifyCorrect =verifyCodeService.verifyEmailByVerifyCode(email,code); if(!verifyCorrect){ log.warn("邮箱验证码错误 ip:{} 邮箱:{}", UserContext.getIp(),email); return Result.fail("邮箱验证码错误"); } String token = loginService.registerUser(userRegister); return Result.ok(token); } @RequestLimit(limitTimes = {1}, durations = {2}, penaltyTimes = {-1}, targets = {LimitTarget.IP}) @PostMapping("/doLogin") public Result login(@Validated @RequestBody UserLoginReq userLogin, HttpServletRequest request) { String code = userLogin.getVerifyCode(); boolean verifyCorrect = verifyCodeService.verifyAndRemoveIfSuccess(request, code); if (!verifyCorrect) { return Result.fail("验证码错误,请重新验证"); } String token = loginService.login(userLogin); return Result.ok(token); } @PostMapping("/logout") public Result logout(HttpServletRequest request) { loginService.logout(request); return Result.ok(); } } ================================================ FILE: acimage_user/src/main/java/com/acimage/user/web/controller/MessageController.java ================================================ package com.acimage.user.web.controller; import com.acimage.common.global.context.UserContext; import com.acimage.common.model.domain.user.UserMsg; import com.acimage.common.result.Result; import com.acimage.user.service.commentmsg.CommentMsgQueryService; import com.acimage.user.service.commentmsg.CommentMsgWriteService; import com.acimage.user.service.usermsg.UserMsgWriteService; import lombok.extern.slf4j.Slf4j; import org.hibernate.validator.constraints.Range; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @Slf4j @RestController @RequestMapping("/api/user/messages/query") @Validated public class MessageController { @Autowired CommentMsgQueryService commentMsgQueryService; @Autowired UserMsgWriteService userMsgWriteService; @GetMapping("/comments/page/{pageNo}/{pageSize}") public Result pageCommentMessages(@Range(min = 1, max = 100, message = "页码在1-100之间") @PathVariable("pageNo") Integer pageNo, @Range(min = 5, max = 10, message = "页大小在5-10之间") @PathVariable("pageSize") Integer pageSize) { userMsgWriteService.resetMsgCount(UserContext.getUserId(), UserMsg::getCommentMsgCount); return Result.ok(commentMsgQueryService.pageMyCommentMsg(UserContext.getUserId(), pageNo, pageSize)); } } ================================================ FILE: acimage_user/src/main/java/com/acimage/user/web/controller/UserOperateController.java ================================================ package com.acimage.user.web.controller; import com.acimage.common.deprecated.annotation.Authentication; import com.acimage.common.redis.annotation.RequestLimit; import com.acimage.common.redis.enums.LimitTarget; import com.acimage.common.result.Result; import com.acimage.user.model.vo.ProfileVo; import com.acimage.user.service.user.UserInfoService; import com.acimage.user.service.user.UserQueryService; import com.acimage.user.service.user.UserWriteService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import javax.servlet.http.HttpServletResponse; import javax.validation.constraints.Size; @Slf4j @RestController @RequestMapping("/api/user/users/operate") @Validated @Authentication public class UserOperateController { @Autowired UserWriteService userWriteService; @RequestLimit(limitTimes = {1}, durations = {2}, penaltyTimes = {-1}, targets = {LimitTarget.USER}) @PutMapping("/username/{username}") public Result modifyUsername(@Size(min = 2, max = 12, message = "用户名长度在2到12之间") @PathVariable String username, HttpServletResponse resp) { String trimUsername = username.trim(); if (trimUsername.length() < 2 || trimUsername.length() > 12) { return Result.fail("用户名有效长度在2-12之间"); } String newToken = userWriteService.updateUsername(trimUsername); return Result.ok(newToken); } } ================================================ FILE: acimage_user/src/main/java/com/acimage/user/web/controller/UserQueryController.java ================================================ package com.acimage.user.web.controller; import com.acimage.common.deprecated.annotation.Authentication; import com.acimage.common.redis.annotation.RequestLimit; import com.acimage.common.redis.enums.LimitTarget; import com.acimage.common.result.Result; import com.acimage.user.model.vo.ProfileVo; import com.acimage.user.service.user.UserInfoService; import com.acimage.user.service.user.UserQueryService; import com.acimage.user.service.user.UserWriteService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import javax.servlet.http.HttpServletResponse; import javax.validation.constraints.Size; @Slf4j @RestController @RequestMapping("/api/user/users/query") @Validated public class UserQueryController { @Autowired UserInfoService userInfoService; @RequestLimit(limitTimes = {15}, durations = {5}, penaltyTimes = {-1}, targets = {LimitTarget.IP}) @GetMapping("/me") public Result me() { return Result.ok(userInfoService.getProfile()); } } ================================================ FILE: acimage_user/src/main/java/com/acimage/user/web/controller/VerifyCodeController.java ================================================ package com.acimage.user.web.controller; import com.acimage.common.result.Result; import com.acimage.user.service.verify.VerifyCodeService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @Slf4j @RestController @Validated @RequestMapping("/api/user/verifies") public class VerifyCodeController { @Autowired VerifyCodeService verifyCodeService; @GetMapping("/commonCode") public Result getCommonVerifyCode(HttpServletRequest request,HttpServletResponse response) { verifyCodeService.writeCodeImageToResponseAndRecord(request,response); return Result.ok(); } } ================================================ FILE: acimage_user/src/main/java/com/acimage/user/web/provider/UserProvider.java ================================================ package com.acimage.user.web.provider; import com.acimage.common.deprecated.annotation.Authentication; import com.acimage.common.global.enums.AuthenticationType; import com.acimage.common.model.domain.user.User; import com.acimage.common.result.Result; import com.acimage.user.service.user.UserQueryService; import com.acimage.user.service.user.UserWriteService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import javax.validation.constraints.NotNull; import javax.validation.constraints.Positive; @Slf4j @RestController @RequestMapping("/user/users") @Validated public class UserProvider { @Autowired UserQueryService userQueryService; @Autowired UserWriteService userWriteService; @GetMapping("/id/{id}") public Result queryUser(@PathVariable @Positive Long id) { User user = userQueryService.getUser(id); return Result.ok(user); } @PutMapping("/photoUrl") Result modifyPhotoUrl(@RequestBody @NotNull String photoUrl){ String newToken=userWriteService.updatePhotoUrl(photoUrl); return Result.ok(newToken); } } ================================================ FILE: acimage_user/src/main/java/com/acimage/user/web/websocket/MyHandshakeInterceptor.java ================================================ package com.acimage.user.web.websocket; import com.acimage.common.global.consts.HeaderKeyConstants; import com.acimage.common.global.context.UserContext; import com.acimage.common.global.exception.NullTokenException; import com.acimage.common.service.TokenService; import com.acimage.common.utils.IpUtils; import com.acimage.common.utils.JwtUtils; import com.acimage.user.global.consts.WebSocketSessionConstants; import com.auth0.jwt.exceptions.JWTVerificationException; import com.auth0.jwt.exceptions.TokenExpiredException; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpResponse; import org.springframework.http.server.ServletServerHttpRequest; import org.springframework.stereotype.Component; import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.server.HandshakeInterceptor; import java.util.*; @Slf4j @Component public class MyHandshakeInterceptor implements HandshakeInterceptor { @Autowired TokenService tokenService; /** * 握手之前,若返回false,则不建立链接 * * * @return */ @Override public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map attributes) { //将用户id放入socket处理器的会话(WebSocketSession)中 ServletServerHttpRequest serverHttpRequest = (ServletServerHttpRequest) request; //获取参数 String token = serverHttpRequest.getServletRequest().getHeader(HeaderKeyConstants.SEC_WEBSOCKET_PROTOCOL); try { JwtUtils.verifyToken(token); } catch (TokenExpiredException e1) { Date date = JwtUtils.getExpire(token); //过时毫秒数 long expireMillis = System.currentTimeMillis() - date.getTime(); //过时限制不超过10s,可以继续解析token long boundMillis = 10 * 1000; if (expireMillis > boundMillis) { return false; } } catch (JWTVerificationException e2) { if (!(e2 instanceof NullTokenException)) { log.warn("非法token"); } return false; } if (!tokenService.hasRecorded(token)) { return false; } //设置当前用户信息 attributes.put(WebSocketSessionConstants.KEY_USER_ID, JwtUtils.getUserId(token)); response.getHeaders().put(HeaderKeyConstants.SEC_WEBSOCKET_PROTOCOL, Collections.singletonList(token)); return true; } @Override public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) { } } ================================================ FILE: acimage_user/src/main/java/com/acimage/user/web/websocket/MyWebSocketHandler.java ================================================ package com.acimage.user.web.websocket; import cn.hutool.core.convert.Convert; import com.acimage.common.global.context.UserContext; import com.acimage.common.service.TokenService; import com.acimage.common.utils.redis.RedisUtils; import com.acimage.user.global.consts.WebSocketSessionConstants; import com.acimage.user.service.usermsg.UserMsgQueryService; import lombok.extern.slf4j.Slf4j; import org.checkerframework.checker.units.qual.A; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.web.socket.*; import org.springframework.web.socket.handler.TextWebSocketHandler; import javax.websocket.Session; import java.io.IOException; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @Slf4j @Component public class MyWebSocketHandler extends TextWebSocketHandler { public static final String WEBSOCKET_KEY = "acimage:user:websocket:online"; @Autowired private RedisUtils redisUtils; @Autowired private UserMsgQueryService userMsgQueryService; public static final ConcurrentMap sessionPool = new ConcurrentHashMap<>(); @Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { long userId = getUserId(session); redisUtils.increment(WEBSOCKET_KEY, 1); log.info("websocket消息: 有新的连接,总数为:{} 用户id:{}", redisUtils.getForString(WEBSOCKET_KEY), userId); sessionPool.put(userId, session); sendMsgCount(userId); } @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { long userId = getUserId(session); redisUtils.increment(WEBSOCKET_KEY, -1); log.info("websocket消息: 关闭连接,总数为:" + redisUtils.getForString(WEBSOCKET_KEY)); try (WebSocketSession webSocketSession = sessionPool.remove(userId)) { } } private long getUserId(WebSocketSession session) { return Convert.convert(Long.class, session.getAttributes().get(WebSocketSessionConstants.KEY_USER_ID)); } public void sendMessage(long userId, int msgNum) { WebSocketSession session = sessionPool.get(userId); if (session != null) { WebSocketMessage message = new TextMessage(Integer.toString(msgNum)); try { session.sendMessage(message); } catch (IOException e) { log.error(e.getMessage()); throw new RuntimeException(e); } } } public void sendMsgCount(long userId) { Integer msgNum = userMsgQueryService.getMsgCount(userId); if (msgNum != null && msgNum != 0) { sendMessage(userId, msgNum); } } @Override public void handleMessage(WebSocketSession session, WebSocketMessage message) throws Exception { log.info("websocket消息: 收到客户端消息:" + message.getPayload()); } } ================================================ FILE: acimage_user/src/main/resources/application-dev.yml ================================================ server: port: 8100 spring: config: activate: on-profile: - dev rabbitmq: host: 192.168.130.128 port: 5672 virtual-host: /acimage username: acimage password: acimage listener: simple: auto-startup: false #消费者是否自动启动 direct: auto-startup: false #生产者是否自动启动 datasource: url: jdbc:mysql://localhost:3306/acimage_user?useSSl=false&allowMultiQueries=true&serverTimezone=UTC username: root password: mysql redis: host: 192.168.130.128 port: 6379 password: redis lettuce: pool: max-active: 8 max-idle: 8 #最大空闲连接 min-idle: 0 #最小空闲连接 max-wait: 100ms #连接等待时间 cloud: nacos: server-addr: localhost:8848 #nacos 服务地址 ================================================ FILE: acimage_user/src/main/resources/application.yml ================================================ spring: profiles: include: common,common-secret active: dev2 servlet: multipart: max-file-size: 3MB max-request-size: 6MB application: name: user-service #服务名称 datasource: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver rabbitmq: mybatis-plus: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.acimage.common.model.domain,com.acimage.user.model.domain, configuration: map-underscore-to-camel-case: true global-config: db-config: table-prefix: tb_ feign: okhttp: enabled: true httpclient: max-connections: 20 # 最大的连接数 max-connections-per-route: 5 # 每个路径的最大连接数 ================================================ FILE: acimage_user/src/main/resources/logback-spring.xml ================================================ %d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%20.20thread{20}] %40logger{40} : %msg%n INFO ACCEPT DENY ${LOG_HOME}/${spring.application.name}/%d{yyyy-MM-dd}-info.%i.log 5MB 8 %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n WARN ACCEPT DENY ${LOG_HOME}/${spring.application.name}/%d{yyyy-MM-dd}-warn.%i.log 5MB 15 %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n ERROR ACCEPT DENY ${LOG_HOME}/${spring.application.name}/%d{yyyy-MM-dd}-error.%i.log 5MB 15 %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n ================================================ FILE: acimage_user/src/main/resources/mapper/CommentMsgMapper.xml ================================================ ================================================ FILE: acimage_user/src/main/resources/mapper/UserMapper.xml ================================================ ================================================ FILE: acimage_user/src/test/java/com/acimage/user/AppTest.java ================================================ package com.acimage.user; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest public class AppTest { @Test public void testApp () { } } ================================================ FILE: doc/sql/.gitignore ================================================ /*.bat ================================================ FILE: doc/sql/acimage_community.sql ================================================ -- MySQL dump 10.13 Distrib 8.0.29, for Win64 (x86_64) -- -- Host: localhost Database: acimage_community -- ------------------------------------------------------ -- Server version 8.0.29 /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; /*!50503 SET NAMES utf8mb4 */; /*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; /*!40103 SET TIME_ZONE='+00:00' */; /*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; -- -- Table structure for table `tb_category` -- DROP TABLE IF EXISTS `tb_category`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `tb_category` ( `id` int NOT NULL AUTO_INCREMENT, `label` varchar(50) NOT NULL, `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `update_time` datetime DEFAULT CURRENT_TIMESTAMP, `deleted` tinyint DEFAULT '0', PRIMARY KEY (`id`), UNIQUE KEY `label` (`label`) ) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Dumping data for table `tb_category` -- LOCK TABLES `tb_category` WRITE; /*!40000 ALTER TABLE `tb_category` DISABLE KEYS */; INSERT INTO `tb_category` VALUES (1,'番剧茶馆','2023-01-28 12:30:59','2023-01-28 12:30:59',0),(2,'漫画杂谈','2023-01-28 12:30:59','2023-01-28 12:30:59',0),(3,'游戏交流','2023-01-28 12:30:59','2023-01-28 12:30:59',0),(4,'新番点评','2023-01-28 12:31:39','2023-01-28 12:31:39',0),(5,'漫画推荐','2023-01-29 18:10:46','2023-01-29 18:10:46',0),(6,'美图分享','2023-01-28 12:31:56','2023-01-28 12:31:56',0),(7,'技术','2023-02-11 22:53:12','2023-02-11 22:53:12',0),(8,'生活','2023-02-12 01:03:46','2023-02-12 01:03:46',0),(9,'其它','2023-02-17 23:32:57','2023-02-17 23:32:57',0); /*!40000 ALTER TABLE `tb_category` ENABLE KEYS */; UNLOCK TABLES; -- -- Table structure for table `tb_cmty_user` -- DROP TABLE IF EXISTS `tb_cmty_user`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `tb_cmty_user` ( `id` bigint NOT NULL, `username` varchar(12) NOT NULL, `photo_url` varchar(60) DEFAULT NULL, `topic_count` int DEFAULT '0', `star_count` int DEFAULT '0', PRIMARY KEY (`id`), UNIQUE KEY `id` (`id`), UNIQUE KEY `username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Dumping data for table `tb_cmty_user` -- LOCK TABLES `tb_cmty_user` WRITE; /*!40000 ALTER TABLE `tb_cmty_user` DISABLE KEYS */; INSERT INTO `tb_cmty_user` VALUES (1572443275490078720,'xlg','/acfile-test/userPhoto/2023/02/15/1625783460550000640.webp',20,2),(1626532367060037632,'真·站长','',1,0),(1626603310067335168,'站长','',0,0); /*!40000 ALTER TABLE `tb_cmty_user` ENABLE KEYS */; UNLOCK TABLES; -- -- Table structure for table `tb_comment` -- DROP TABLE IF EXISTS `tb_comment`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `tb_comment` ( `id` bigint NOT NULL, `topic_id` bigint NOT NULL, `user_id` bigint NOT NULL, `content` varchar(200) DEFAULT NULL, `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `update_time` datetime DEFAULT CURRENT_TIMESTAMP, `deleted` tinyint DEFAULT '0', PRIMARY KEY (`id`), KEY `fk_comment_user_id` (`user_id`), KEY `fk_comment_topic_id` (`topic_id`), CONSTRAINT `fk_comment_topic_id` FOREIGN KEY (`topic_id`) REFERENCES `tb_topic` (`id`), CONSTRAINT `fk_comment_user_id` FOREIGN KEY (`user_id`) REFERENCES `tb_cmty_user` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Dumping data for table `tb_comment` -- LOCK TABLES `tb_comment` WRITE; /*!40000 ALTER TABLE `tb_comment` DISABLE KEYS */; /*!40000 ALTER TABLE `tb_comment` ENABLE KEYS */; UNLOCK TABLES; -- -- Table structure for table `tb_home_carousel` -- DROP TABLE IF EXISTS `tb_home_carousel`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `tb_home_carousel` ( `id` int NOT NULL AUTO_INCREMENT, `description` varchar(30) DEFAULT NULL, `url` varchar(60) NOT NULL, `link` varchar(100) NOT NULL, `size` int DEFAULT NULL, `location` int DEFAULT NULL, `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `update_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Dumping data for table `tb_home_carousel` -- LOCK TABLES `tb_home_carousel` WRITE; /*!40000 ALTER TABLE `tb_home_carousel` DISABLE KEYS */; /*!40000 ALTER TABLE `tb_home_carousel` ENABLE KEYS */; UNLOCK TABLES; -- -- Table structure for table `tb_star` -- DROP TABLE IF EXISTS `tb_star`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `tb_star` ( `user_id` bigint NOT NULL, `topic_id` bigint NOT NULL, `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`user_id`,`topic_id`), KEY `index_star_topic_id` (`topic_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Dumping data for table `tb_star` -- LOCK TABLES `tb_star` WRITE; /*!40000 ALTER TABLE `tb_star` DISABLE KEYS */; /*!40000 ALTER TABLE `tb_star` ENABLE KEYS */; UNLOCK TABLES; -- -- Table structure for table `tb_tag` -- DROP TABLE IF EXISTS `tb_tag`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `tb_tag` ( `id` int NOT NULL AUTO_INCREMENT, `label` varchar(30) NOT NULL, `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `update_time` datetime DEFAULT CURRENT_TIMESTAMP, `deleted` tinyint DEFAULT '0', PRIMARY KEY (`id`), UNIQUE KEY `label` (`label`) ) ENGINE=InnoDB AUTO_INCREMENT=26 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Dumping data for table `tb_tag` -- LOCK TABLES `tb_tag` WRITE; /*!40000 ALTER TABLE `tb_tag` DISABLE KEYS */; INSERT INTO `tb_tag` VALUES (1,'冒险','2023-01-28 16:51:16','2023-01-28 16:51:16',0),(2,'恋爱','2023-01-28 16:51:16','2023-01-28 16:51:16',0),(3,'剧情','2023-01-28 16:51:16','2023-01-28 16:51:16',0),(4,'搞笑','2023-01-28 16:51:16','2023-01-28 16:51:16',0),(5,'交流','2023-01-28 16:51:16','2023-01-28 16:51:16',0),(6,'温馨','2023-01-28 16:51:16','2023-01-28 16:51:16',0),(7,'原创','2023-01-28 16:51:16','2023-01-28 16:51:16',0),(8,'热血','2023-01-28 16:51:16','2023-01-28 16:51:16',0),(9,'科幻','2023-01-28 16:51:16','2023-01-28 16:51:16',0),(10,'运动','2023-01-28 16:51:16','2023-01-28 16:51:16',0),(11,'竞技','2023-01-28 16:51:16','2023-01-28 16:51:16',0),(12,'吐槽','2023-01-28 16:51:16','2023-01-28 16:51:16',0),(13,'治愈','2023-01-28 16:51:16','2023-01-28 16:51:16',0),(14,'致郁','2023-01-28 16:51:16','2023-01-28 16:51:16',0),(15,'音乐','2023-01-28 16:51:16','2023-01-28 16:51:16',0),(16,'美食','2023-01-28 16:51:16','2023-01-28 16:51:16',0),(17,'异世界','2023-01-28 16:51:16','2023-01-28 16:51:16',0),(18,'日常','2023-01-28 16:51:16','2023-01-28 16:51:16',0),(19,'校园','2023-01-28 16:51:16','2023-01-28 16:51:16',0),(20,'资讯','2023-01-28 16:51:16','2023-01-28 16:51:16',0),(21,'新番','2023-01-28 16:54:03','2023-01-28 16:54:03',0),(22,'老番','2023-01-28 16:54:03','2023-01-28 16:54:03',0),(23,'技术','2023-02-12 01:34:03','2023-02-12 01:34:03',0),(24,'超能力','2023-02-15 21:25:50','2023-02-15 21:25:50',0),(25,'美少女','2023-02-15 21:26:05','2023-02-15 21:26:05',0); /*!40000 ALTER TABLE `tb_tag` ENABLE KEYS */; UNLOCK TABLES; -- -- Table structure for table `tb_tag_topic` -- DROP TABLE IF EXISTS `tb_tag_topic`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `tb_tag_topic` ( `id` bigint NOT NULL, `topic_id` bigint NOT NULL, `tag_id` int NOT NULL, `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `deleted` tinyint DEFAULT '0', PRIMARY KEY (`id`), KEY `topic_id` (`topic_id`), KEY `tag_id` (`tag_id`), CONSTRAINT `tb_tag_topic_ibfk_1` FOREIGN KEY (`topic_id`) REFERENCES `tb_topic` (`id`), CONSTRAINT `tb_tag_topic_ibfk_2` FOREIGN KEY (`tag_id`) REFERENCES `tb_tag` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Dumping data for table `tb_tag_topic` -- LOCK TABLES `tb_tag_topic` WRITE; /*!40000 ALTER TABLE `tb_tag_topic` DISABLE KEYS */; /*!40000 ALTER TABLE `tb_tag_topic` ENABLE KEYS */; UNLOCK TABLES; -- -- Table structure for table `tb_topic` -- DROP TABLE IF EXISTS `tb_topic`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `tb_topic` ( `id` bigint NOT NULL, `user_id` bigint NOT NULL, `title` varchar(30) NOT NULL COMMENT '长度≥4', `content` varchar(600) NOT NULL, `cover_image_url` varchar(100) DEFAULT NULL, `star_count` int DEFAULT '0', `comment_count` int DEFAULT '0', `page_view` int DEFAULT '0', `activity_time` datetime DEFAULT CURRENT_TIMESTAMP, `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `update_time` datetime DEFAULT CURRENT_TIMESTAMP, `deleted` tinyint DEFAULT '0', `category_id` int NOT NULL, PRIMARY KEY (`id`), KEY `fk_topic_user_id` (`user_id`), KEY `fk_topic_category_id_idx` (`category_id`), CONSTRAINT `fk_topic_category_id` FOREIGN KEY (`category_id`) REFERENCES `tb_category` (`id`), CONSTRAINT `fk_topic_user_id` FOREIGN KEY (`user_id`) REFERENCES `tb_cmty_user` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Dumping data for table `tb_topic` -- LOCK TABLES `tb_topic` WRITE; /*!40000 ALTER TABLE `tb_topic` DISABLE KEYS */; /*!40000 ALTER TABLE `tb_topic` ENABLE KEYS */; UNLOCK TABLES; -- -- Table structure for table `tb_topic_html` -- DROP TABLE IF EXISTS `tb_topic_html`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `tb_topic_html` ( `topic_id` bigint NOT NULL, `html` varchar(5000) NOT NULL, `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `update_time` datetime DEFAULT CURRENT_TIMESTAMP, `deleted` tinyint DEFAULT '0', PRIMARY KEY (`topic_id`), CONSTRAINT `tb_topic_html_ibfk_1` FOREIGN KEY (`topic_id`) REFERENCES `tb_topic` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Dumping data for table `tb_topic_html` -- LOCK TABLES `tb_topic_html` WRITE; /*!40000 ALTER TABLE `tb_topic_html` DISABLE KEYS */; /*!40000 ALTER TABLE `tb_topic_html` ENABLE KEYS */; UNLOCK TABLES; /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; /*!40101 SET SQL_MODE=@OLD_SQL_MODE */; /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; /*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; -- Dump completed on 2023-02-17 23:34:07 ================================================ FILE: doc/sql/acimage_image.sql ================================================ -- MySQL dump 10.13 Distrib 8.0.29, for Win64 (x86_64) -- -- Host: localhost Database: acimage_image -- ------------------------------------------------------ -- Server version 8.0.29 /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; /*!50503 SET NAMES utf8mb4 */; /*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; /*!40103 SET TIME_ZONE='+00:00' */; /*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; -- -- Table structure for table `tb_image` -- DROP TABLE IF EXISTS `tb_image`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `tb_image` ( `id` bigint NOT NULL, `topic_id` bigint DEFAULT NULL, `url` varchar(80) NOT NULL, `size` int NOT NULL, `description` varchar(30) DEFAULT NULL, `file_name` varchar(80) DEFAULT NULL, `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `update_time` datetime DEFAULT CURRENT_TIMESTAMP, `deleted` tinyint DEFAULT '0', PRIMARY KEY (`id`), KEY `index_image_url` (`url`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Dumping data for table `tb_image` -- LOCK TABLES `tb_image` WRITE; /*!40000 ALTER TABLE `tb_image` DISABLE KEYS */; /*!40000 ALTER TABLE `tb_image` ENABLE KEYS */; UNLOCK TABLES; -- -- Table structure for table `tb_image_hash` -- DROP TABLE IF EXISTS `tb_image_hash`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `tb_image_hash` ( `image_id` bigint NOT NULL, `hash_value` bigint NOT NULL COMMENT '图片哈希值,逻辑上表示为64个比特,话题里的图片都会被哈希处理,用于识图功能', `hash_sum` int NOT NULL COMMENT '哈希值的比特之和,当两张图哈希距离较小时,哈希和应该比较接近,用来过滤', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`image_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Dumping data for table `tb_image_hash` -- LOCK TABLES `tb_image_hash` WRITE; /*!40000 ALTER TABLE `tb_image_hash` DISABLE KEYS */; /*!40000 ALTER TABLE `tb_image_hash` ENABLE KEYS */; UNLOCK TABLES; /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; /*!40101 SET SQL_MODE=@OLD_SQL_MODE */; /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; /*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; -- Dump completed on 2023-02-17 23:34:07 ================================================ FILE: doc/sql/acimage_sys.sql ================================================ -- MySQL dump 10.13 Distrib 8.0.29, for Win64 (x86_64) -- -- Host: localhost Database: acimage_sys -- ------------------------------------------------------ -- Server version 8.0.29 /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; /*!50503 SET NAMES utf8mb4 */; /*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; /*!40103 SET TIME_ZONE='+00:00' */; /*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; -- -- Table structure for table `tb_api` -- DROP TABLE IF EXISTS `tb_api`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `tb_api` ( `id` int NOT NULL AUTO_INCREMENT, `path` varchar(200) NOT NULL, `method` varchar(20) NOT NULL, `permission_id` int NOT NULL, `enable` tinyint DEFAULT '1', `note` varchar(100) DEFAULT NULL, `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `update_time` datetime DEFAULT CURRENT_TIMESTAMP, `deleted` tinyint(1) DEFAULT '0', PRIMARY KEY (`id`), KEY `fk_api_permissionId` (`permission_id`), CONSTRAINT `fk_api_permissionId` FOREIGN KEY (`permission_id`) REFERENCES `tb_permission` (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=35 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Dumping data for table `tb_api` -- LOCK TABLES `tb_api` WRITE; /*!40000 ALTER TABLE `tb_api` DISABLE KEYS */; INSERT INTO `tb_api` VALUES (6,'/api/community/topics/query/**','ALL',30,1,'查询话题','2023-02-06 14:15:49','2023-02-06 14:15:49',0),(7,'/api/community/topics/operate/**','ALL',35,1,'操作话题','2023-02-06 10:24:10','2023-02-06 10:24:10',0),(9,'/api/community/*/xxx','GET',36,1,NULL,'2023-02-06 09:52:13','2023-02-06 09:52:13',1),(10,'/api/community/topics/operate','POST',11,1,'发表话题','2023-02-06 10:16:05','2023-02-06 10:16:05',0),(11,'/api/community/comments/query/**','GET',29,1,'查看话题','2023-02-06 10:24:17','2023-02-06 10:24:17',0),(12,'/api/community/comments/operate/**','ALL',34,1,'操作话题','2023-02-06 15:07:01','2023-02-06 15:07:01',0),(13,'/api/community/categories/query/**','GET',42,1,'分类查询','2023-02-06 14:21:10','2023-02-06 14:21:10',0),(14,'/api/community/tags/query/**','GET',42,1,'标签查询','2023-02-06 14:20:47','2023-02-06 14:20:47',0),(15,'/api/community/stars/query/**','GET',43,1,'查询点赞','2023-02-06 14:53:29','2023-02-06 14:53:29',0),(16,'/api/community/stars/operate/**','ALL',44,1,'star操作','2023-02-06 14:57:27','2023-02-06 14:57:27',0),(17,'/api/community/topics/search/**','ALL',36,1,'搜索话题','2023-02-11 07:08:16','2023-02-11 07:08:16',0),(18,'/api/user/logins/**','ALL',47,1,'登录注册等','2023-02-09 08:58:18','2023-02-09 08:58:18',0),(19,'/api/user/verifies/**','ALL',47,1,'请求验证码','2023-02-09 09:00:41','2023-02-09 09:00:41',0),(20,'/api/image/homeCarousels/**','GET',47,1,'轮播图','2023-02-09 13:58:41','2023-02-09 13:58:41',1),(21,'/api/image/images/operate/**','ALL',35,1,'上传话题图片','2023-02-11 07:06:38','2023-02-11 07:06:38',0),(22,'/api/user/**','GET',43,1,NULL,'2023-02-09 15:14:00','2023-02-09 15:14:00',1),(23,'/api/user/users/query/**','GET',43,1,'查询用户信息','2023-02-14 06:02:16','2023-02-14 06:02:16',0),(24,'/api/user/users/operate/**','ALL',44,1,'用户操作自身信息','2023-02-09 15:24:48','2023-02-09 15:24:48',0),(25,'/api/community/users/rank/**','GET',47,1,'查询用户排名','2023-02-11 07:07:49','2023-02-11 07:07:49',0),(26,'/api/image/photos/operate/**','ALL',44,1,'头像操作','2023-02-11 07:07:37','2023-02-11 07:07:37',0),(27,'/api/admin/**','ALL',46,1,'admin操作','2023-02-11 12:22:04','2023-02-11 12:22:04',0),(28,'/api/community/topics/query/mine/**','GET',43,1,'查询用户话题','2023-02-11 13:57:07','2023-02-11 13:57:07',0),(29,'/api/community/homeCarousels/**','GET',47,1,'查询首页','2023-02-14 06:02:38','2023-02-14 06:02:38',0),(30,'/api/image/images/searchByImage','ALL',43,1,'以图搜图','2023-02-14 09:53:50','2023-02-14 09:53:50',0),(31,'/api/community/comments/query/mine/**','GET',43,1,'查询自己话题','2023-02-14 13:39:47','2023-02-14 13:39:47',0),(32,'/api/community/stars/query/mine/**','GET',43,1,'查询自己点赞','2023-02-14 13:41:42','2023-02-14 13:41:42',0),(33,'/api/admin/authorizes/operate/**','ALL',27,1,'操作权限','2023-02-15 08:49:16','2023-02-15 08:49:16',0),(34,'/api/admin/logins/**','ALL',47,1,'管理员登录','2023-02-15 08:51:42','2023-02-15 08:51:42',0); /*!40000 ALTER TABLE `tb_api` ENABLE KEYS */; UNLOCK TABLES; -- -- Table structure for table `tb_authorize` -- DROP TABLE IF EXISTS `tb_authorize`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `tb_authorize` ( `id` int NOT NULL AUTO_INCREMENT, `role_id` int NOT NULL, `permission_id` int NOT NULL, `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_role_permission` (`role_id`,`permission_id`), KEY `fk_authorize_permission_id_idx` (`permission_id`), CONSTRAINT `fk_authorize_permission_id` FOREIGN KEY (`permission_id`) REFERENCES `tb_permission` (`id`), CONSTRAINT `tb_authorize_ibfk_1` FOREIGN KEY (`role_id`) REFERENCES `tb_role` (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=87 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Dumping data for table `tb_authorize` -- LOCK TABLES `tb_authorize` WRITE; /*!40000 ALTER TABLE `tb_authorize` DISABLE KEYS */; INSERT INTO `tb_authorize` VALUES (2,2,14,'2023-01-16 22:31:31'),(3,1,15,'2023-01-16 22:32:06'),(10,3,18,'2023-01-16 22:37:00'),(12,3,21,'2023-01-16 22:37:04'),(13,3,22,'2023-01-16 22:37:05'),(14,3,24,'2023-01-16 22:37:07'),(15,2,29,'2023-01-16 23:56:39'),(16,2,30,'2023-01-16 23:56:46'),(17,1,29,'2023-01-16 23:58:10'),(18,1,30,'2023-01-16 23:58:12'),(19,3,31,'2023-01-16 23:58:18'),(20,3,32,'2023-01-16 23:58:21'),(21,3,33,'2023-01-16 23:58:26'),(22,3,26,'2023-01-16 23:58:28'),(23,6,18,'2023-01-16 23:58:55'),(25,6,31,'2023-01-16 23:58:58'),(26,6,21,'2023-01-16 23:58:59'),(27,6,22,'2023-01-16 23:59:01'),(28,6,32,'2023-01-16 23:59:02'),(29,6,24,'2023-01-16 23:59:04'),(30,6,33,'2023-01-16 23:59:06'),(31,6,26,'2023-01-16 23:59:08'),(34,6,27,'2023-02-03 22:01:50'),(36,6,11,'2023-02-03 22:02:28'),(40,2,42,'2023-02-05 23:54:08'),(41,2,36,'2023-02-05 23:55:04'),(42,1,35,'2023-02-05 23:55:41'),(43,1,36,'2023-02-05 23:55:42'),(44,1,42,'2023-02-05 23:55:45'),(45,1,34,'2023-02-05 23:56:02'),(47,7,11,'2023-02-06 18:14:56'),(48,1,43,'2023-02-06 22:54:10'),(49,1,44,'2023-02-06 22:55:41'),(51,2,47,'2023-02-09 16:55:38'),(52,1,47,'2023-02-09 16:55:45'),(53,3,47,'2023-02-09 16:55:52'),(54,6,47,'2023-02-09 16:55:56'),(55,7,47,'2023-02-09 16:56:01'),(56,6,46,'2023-02-11 20:22:30'),(57,7,35,'2023-02-11 20:23:07'),(58,7,36,'2023-02-11 20:23:09'),(59,7,30,'2023-02-11 20:23:11'),(60,7,34,'2023-02-11 20:23:12'),(61,7,29,'2023-02-11 20:23:14'),(63,7,43,'2023-02-11 20:24:10'),(64,7,44,'2023-02-11 20:24:11'),(65,7,15,'2023-02-11 20:24:18'),(66,3,15,'2023-02-11 20:24:36'),(67,3,29,'2023-02-11 20:24:37'),(68,3,34,'2023-02-11 20:24:38'),(69,3,11,'2023-02-11 20:24:40'),(70,3,30,'2023-02-11 20:24:43'),(71,3,35,'2023-02-11 20:24:44'),(72,3,36,'2023-02-11 20:24:47'),(73,3,42,'2023-02-11 20:24:48'),(74,3,43,'2023-02-11 20:25:01'),(75,3,44,'2023-02-11 20:25:04'),(76,6,29,'2023-02-11 20:25:21'),(77,6,34,'2023-02-11 20:25:23'),(78,6,15,'2023-02-11 20:25:25'),(79,6,30,'2023-02-11 20:25:26'),(80,6,35,'2023-02-11 20:25:28'),(81,6,36,'2023-02-11 20:25:29'),(82,6,42,'2023-02-11 20:25:31'),(83,6,43,'2023-02-11 20:25:35'),(84,6,44,'2023-02-11 20:25:37'),(86,1,11,'2023-02-14 22:28:44'); /*!40000 ALTER TABLE `tb_authorize` ENABLE KEYS */; UNLOCK TABLES; -- -- Table structure for table `tb_permission` -- DROP TABLE IF EXISTS `tb_permission`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `tb_permission` ( `id` int NOT NULL AUTO_INCREMENT, `parent_id` int DEFAULT NULL, `code` varchar(50) DEFAULT NULL, `note` varchar(20) DEFAULT NULL, `module` tinyint NOT NULL COMMENT '是权限模块还是处于叶子节点的权限', `label` varchar(20) NOT NULL, `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `update_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `fk_tb_permission_parent_id_idx` (`parent_id`), CONSTRAINT `fk_tb_permission_parent_id` FOREIGN KEY (`parent_id`) REFERENCES `tb_permission` (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=48 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Dumping data for table `tb_permission` -- LOCK TABLES `tb_permission` WRITE; /*!40000 ALTER TABLE `tb_permission` DISABLE KEYS */; INSERT INTO `tb_permission` VALUES (1,NULL,NULL,'用户社区权限模块',1,'社区','2023-01-16 16:16:53','2023-01-16 15:06:42'),(2,1,NULL,'',1,'用户','2023-01-16 16:36:49','2023-01-16 15:06:59'),(3,1,NULL,'',1,'评论','2023-01-16 16:37:48','2023-01-16 15:07:04'),(4,NULL,NULL,'',1,'管理','2023-01-16 16:43:22','2023-01-16 15:06:53'),(6,1,NULL,'',1,'话题','2023-01-16 16:46:07','2023-01-16 15:07:13'),(11,6,'topic:add','',0,'发表话题','2023-01-16 17:01:47','2023-01-16 15:07:47'),(14,2,'user:register','',0,'注册','2023-01-16 17:13:43','2023-01-16 15:08:17'),(15,2,'user:update','和用户隐私信息修改不同',0,'用户基本信息修改','2023-01-16 17:15:01','2023-01-16 15:08:38'),(16,4,NULL,'',1,'社区管理','2023-01-16 17:16:00','2023-01-16 15:51:03'),(17,16,NULL,'',1,'评论管理','2023-01-16 17:18:39','2023-01-16 15:51:10'),(18,17,'system:comment:operate','',0,'操作评论-管理','2023-01-16 17:19:38','2023-02-05 10:00:25'),(20,16,NULL,'',1,'话题管理','2023-01-16 17:21:21','2023-01-16 15:51:23'),(21,20,'system:topic:operate','',0,'操作话题-管理','2023-01-16 17:22:17','2023-02-05 09:50:20'),(22,20,'system:topic:delete','',0,'话题删除-管理','2023-01-16 17:23:26','2023-01-16 17:23:26'),(23,16,NULL,'',1,'用户管理','2023-01-16 17:25:22','2023-01-16 15:51:46'),(24,23,'system:user:operate','',0,'操作用户信息-管理','2023-01-16 17:26:16','2023-02-05 09:49:42'),(25,4,NULL,'',1,'权限管理','2023-01-16 18:18:56','2023-01-16 15:51:35'),(26,25,'auth:query','',0,'权限查看','2023-01-16 22:43:42','2023-01-16 22:43:42'),(27,25,'auth:operate','',0,'操作权限','2023-01-16 22:44:32','2023-02-05 10:01:43'),(29,3,'comment:query','',0,'查看评论','2023-01-16 22:47:19','2023-01-16 15:51:55'),(30,6,'topic:query','',0,'查看话题','2023-01-16 22:49:17','2023-01-16 15:57:03'),(31,17,'system:comment:query','',0,'查看评论-管理','2023-01-16 23:53:10','2023-01-16 15:53:25'),(32,20,'system:topic:query','',0,'查看话题-管理','2023-01-16 23:54:00','2023-01-16 15:54:19'),(33,23,'system:user:query','',0,'查看用户-管理','2023-01-16 23:55:02','2023-01-16 23:55:02'),(34,3,'comment:operate','',0,'操作评论','2023-02-05 17:44:46','2023-02-05 17:44:46'),(35,6,'topic:operate','',0,'操作话题','2023-02-05 17:48:34','2023-02-05 17:48:34'),(36,6,'topic:search','',0,'搜索话题','2023-02-05 18:02:27','2023-02-05 18:02:27'),(41,1,NULL,'',1,'总体','2023-02-05 23:51:30','2023-02-08 13:53:26'),(42,41,'visitor:base:query','',0,'基本查询','2023-02-05 23:51:54','2023-02-06 14:56:16'),(43,41,'user:base:query','',0,'注册用户基本查询权限','2023-02-06 22:52:44','2023-02-06 22:52:44'),(44,41,'user:base:operate','',0,'注册用户基本操作权限','2023-02-06 22:55:30','2023-02-06 22:55:30'),(45,4,NULL,'',1,'总体管理','2023-02-08 21:54:02','2023-02-08 21:54:02'),(46,45,'admin:operate','',0,'管理员权限','2023-02-08 21:54:37','2023-02-08 21:54:37'),(47,41,'none','',0,'无权限','2023-02-09 16:51:13','2023-02-09 16:51:13'); /*!40000 ALTER TABLE `tb_permission` ENABLE KEYS */; UNLOCK TABLES; -- -- Table structure for table `tb_role` -- DROP TABLE IF EXISTS `tb_role`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `tb_role` ( `id` int NOT NULL AUTO_INCREMENT, `role_name` varchar(20) NOT NULL, `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `update_time` datetime DEFAULT CURRENT_TIMESTAMP, `note` varchar(20) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `role_name` (`role_name`) ) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Dumping data for table `tb_role` -- LOCK TABLES `tb_role` WRITE; /*!40000 ALTER TABLE `tb_role` DISABLE KEYS */; INSERT INTO `tb_role` VALUES (1,'user','2023-01-15 18:22:25','2023-01-21 14:15:59','社区用户'),(2,'visitor','2023-01-15 18:30:03','2023-01-15 18:30:03','游客'),(3,'admin','2023-01-15 18:30:15','2023-01-15 18:30:15','管理员'),(6,'superAdmin','2023-01-16 23:58:44','2023-01-16 23:58:44','超级管理员'),(7,'verifiedUser','2023-02-05 23:57:55','2023-02-05 23:57:55','认证用户'); /*!40000 ALTER TABLE `tb_role` ENABLE KEYS */; UNLOCK TABLES; -- -- Table structure for table `tb_user_role` -- DROP TABLE IF EXISTS `tb_user_role`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `tb_user_role` ( `id` bigint NOT NULL, `user_id` mediumtext NOT NULL, `role_id` int NOT NULL, `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `role_id` (`role_id`), CONSTRAINT `tb_user_role_ibfk_1` FOREIGN KEY (`role_id`) REFERENCES `tb_role` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Dumping data for table `tb_user_role` -- LOCK TABLES `tb_user_role` WRITE; /*!40000 ALTER TABLE `tb_user_role` DISABLE KEYS */; INSERT INTO `tb_user_role` VALUES (1621894312265109504,'1572443275490078720',6,'2023-02-04 15:31:52'),(1625875967283638272,'1572443275490078720',3,'2023-02-15 15:13:33'),(1626533157371764736,'1626532367060037632',3,'2023-02-17 18:44:59'),(1626533175579238400,'1626532367060037632',6,'2023-02-17 18:45:03'),(1626603759369457664,'1626603310067335168',3,'2023-02-17 23:25:32'),(1626603780563275776,'1626603310067335168',6,'2023-02-17 23:25:37'); /*!40000 ALTER TABLE `tb_user_role` ENABLE KEYS */; UNLOCK TABLES; /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; /*!40101 SET SQL_MODE=@OLD_SQL_MODE */; /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; /*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; -- Dump completed on 2023-02-17 23:34:07 ================================================ FILE: doc/sql/acimage_user.sql ================================================ -- MySQL dump 10.13 Distrib 8.0.29, for Win64 (x86_64) -- -- Host: localhost Database: acimage_user -- ------------------------------------------------------ -- Server version 8.0.29 /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; /*!50503 SET NAMES utf8mb4 */; /*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; /*!40103 SET TIME_ZONE='+00:00' */; /*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; -- -- Table structure for table `tb_user` -- DROP TABLE IF EXISTS `tb_user`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `tb_user` ( `id` bigint NOT NULL, `username` varchar(12) NOT NULL, `photo_url` varchar(60) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `id` (`id`), UNIQUE KEY `username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Dumping data for table `tb_user` -- LOCK TABLES `tb_user` WRITE; /*!40000 ALTER TABLE `tb_user` DISABLE KEYS */; INSERT INTO `tb_user` VALUES (1626532367060037632,'真·站长',NULL),(1626603310067335168,'站长',NULL); /*!40000 ALTER TABLE `tb_user` ENABLE KEYS */; UNLOCK TABLES; -- -- Table structure for table `tb_user_msg` -- DROP TABLE IF EXISTS `tb_user_msg`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `tb_user_msg` ( `user_id` bigint NOT NULL, `star_msg_count` int DEFAULT '0', `read_star_time` datetime DEFAULT CURRENT_TIMESTAMP, `comment_msg_count` int DEFAULT '0', `read_comment_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`user_id`), CONSTRAINT `tb_user_msg_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `tb_user` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Dumping data for table `tb_user_msg` -- LOCK TABLES `tb_user_msg` WRITE; /*!40000 ALTER TABLE `tb_user_msg` DISABLE KEYS */; /*!40000 ALTER TABLE `tb_user_msg` ENABLE KEYS */; UNLOCK TABLES; -- -- Table structure for table `tb_user_privacy` -- DROP TABLE IF EXISTS `tb_user_privacy`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `tb_user_privacy` ( `user_id` bigint NOT NULL, `pwd` char(32) NOT NULL, `salt` char(32) NOT NULL, `email` varchar(32) NOT NULL, `register_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`user_id`), UNIQUE KEY `id` (`user_id`), UNIQUE KEY `email` (`email`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Dumping data for table `tb_user_privacy` -- LOCK TABLES `tb_user_privacy` WRITE; /*!40000 ALTER TABLE `tb_user_privacy` DISABLE KEYS */; INSERT INTO `tb_user_privacy` VALUES (1626532367060037632,'b5c2e07c00c75534a925c5952f060419','794187bad2594716b375bec82f881b4f','xlg@qq.com','2023-02-17 18:41:50'),(1626603310067335168,'eb4285b7c07ddecedfa07dffa1f74b8d','db7f6862f4884ae1b30fc6b86aee5735','xlg1@qq.com','2023-02-17 23:23:44'); /*!40000 ALTER TABLE `tb_user_privacy` ENABLE KEYS */; UNLOCK TABLES; /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; /*!40101 SET SQL_MODE=@OLD_SQL_MODE */; /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; /*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; -- Dump completed on 2023-02-17 23:34:07 ================================================ FILE: pom.xml ================================================ 4.0.0 pom acimage_community acimage_image acimage_common acimage_gateway acimage_admin acimage_feign acimage_user org.springframework.boot spring-boot-starter-parent 2.6.11 com.acimage acimage 0.0.1-SNAPSHOT acimage community to topic acg images UTF-8 8 8 1.8 2021.0.3 2.6.11 2.0.1.RELEASE 1.2.9 3.4.1 2.2.5.RELEASE 2021.0.1.0 3.8.3 5.8.6 8.0.30 3.1.1 3.1.1 [7.7.0, 7.10.99] 2.0.21 3.5.1 8.2.1 0.2.0 3.0.3.1 7.12.0 4.2.12 0.4.8 2.8.8 org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-devtools true org.projectlombok lombok org.springframework.cloud spring-cloud-dependencies ${springcloud.version} pom import com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery ${nacos.version} com.alibaba.cloud spring-cloud-alibaba-dependencies ${nacos.version} pom import com.alibaba.cloud spring-cloud-starter-alibaba-sentinel ${sentinel.version} com.qiniu qiniu-java-sdk ${qiniu.version} com.alibaba druid ${druid.version} com.baomidou mybatis-plus-boot-starter ${mybatis-plus.version} com.auth0 java-jwt ${jwt.version} cn.hutool hutool-all ${hutool.version} mysql mysql-connector-java ${mysql-connector.version} org.springframework.cloud spring-cloud-starter-openfeign ${openfeign.version} org.springframework.cloud spring-cloud-starter-loadbalancer ${loadbalancer.version} com.github.ben-manes.caffeine caffeine ${caffeine.version} com.baomidou dynamic-datasource-spring-boot-starter ${dynamic-datesource.version} io.minio minio ${minio.version} com.github.houbb sensitive-word ${sensitive-word.version} io.github.toolgood toolgood-words ${toolgood-words.version} org.springframework.data spring-data-elasticsearch ${data-elasticsearch.version} net.coobird thumbnailator ${thumbnailator.version} org.springframework.boot spring-boot-maven-plugin ${springboot.version} maven-clean-plugin 3.1.0 maven-resources-plugin 3.0.2 maven-compiler-plugin 3.8.0 maven-surefire-plugin 2.22.2 maven-jar-plugin 3.0.2 maven-install-plugin 2.5.2 maven-deploy-plugin 2.8.2 maven-site-plugin 3.7.1 maven-project-info-reports-plugin 3.0.0 spring-milestones Spring Milestones https://repo.spring.io/milestone false spring-snapshots Spring Snapshots https://repo.spring.io/snapshot false spring-milestones Spring Milestones https://repo.spring.io/milestone false spring-snapshots Spring Snapshots https://repo.spring.io/snapshot false ================================================ FILE: vue-manage-system/.github/FUNDING.yml ================================================ # These are supported funding model platforms github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username otechie: # Replace with a single Otechie username custom: https://lin-xin.gitee.io/images/weixin.jpg ================================================ FILE: vue-manage-system/.gitignore ================================================ .DS_Store node_modules /dist # local env files .env.local .env.*.local # Log files npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* # Editor directories and files .idea .vscode *.suo *.ntvs* *.njsproj *.sln *.sw? ================================================ FILE: vue-manage-system/LICENSE ================================================ MIT License Copyright (c) 2016-2023 vue-manage-system Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: vue-manage-system/README.md ================================================ # vue-manage-system vue pinia license GitHub release donate 基于 Vue3 + pinia + Element Plus 的后台管理系统解决方案。[线上地址](https://lin-xin.gitee.io/example/work/) > Vue2 版本请看 [tag-V4.2.0](https://github.com/lin-xin/vue-manage-system/tree/V4.2.0) [English document](https://github.com/lin-xin/manage-system/blob/master/README_EN.md) ## 赞助商 ### 好问 [](https://www.bestqa.net/home/index.html) 专业问卷服务,一对一客服,按需定制 ## 支持作者 请作者喝杯咖啡吧!(微信号:linxin_20) ![微信扫一扫](https://lin-xin.gitee.io/images/weixin.jpg) ## 前言 该方案作为一套多功能的后台框架模板,适用于绝大部分的后台管理系统开发。基于 Vue3 + pinia + typescript,引用 Element Plus 组件库,方便开发。实现逻辑简单,适合外包项目,快速交付。 ## 功能 - [x] Element Plus - [x] vite 3 - [x] pinia - [x] typescript - [x] 登录/注销 - [x] Dashboard - [x] 表格 - [x] Tab 选项卡 - [x] 表单 - [x] 图表 :bar_chart: - [x] 富文本/markdown编辑器 - [x] 图片拖拽/裁剪上传 - [x] 权限管理 - [x] 三级菜单 - [x] 自定义图标 ## 安装步骤 > 因为使用vite3,node版本需要 14.18+ ``` git clone https://github.com/lin-xin/vue-manage-system.git // 把模板下载到本地 cd vue-manage-system // 进入模板目录 npm install // 安装项目依赖,等待安装完成之后,安装失败可用 cnpm 或 yarn // 运行 npm run dev // 执行构建命令,生成的dist文件夹放在服务器下即可访问 npm run build ``` ## 组件使用说明与演示 ### vue-schart vue.js 封装 sChart.js 的图表组件。访问地址:[vue-schart](https://github.com/lin-xin/vue-schart#/)

Downloads

```html ``` ## 项目截图 ### 登录 ![Image text](https://github.com/lin-xin/manage-system/raw/master/screenshots/wms3.png) ### 首页 ![Image text](https://github.com/lin-xin/manage-system/raw/master/screenshots/wms1.png) ## License [MIT](https://github.com/lin-xin/vue-manage-system/blob/master/LICENSE) ================================================ FILE: vue-manage-system/README_EN.md ================================================ # vue-manage-system vue element-ui license GitHub release donate The web management system solution based on Vue3 and ElementPlus。[live demo](https://lin-xin.gitee.io/example/work/) Please check the version of vue2 in [tag V4.2.0](https://github.com/lin-xin/vue-manage-system/tree/V4.2.0) ## Donation ![WeChat](https://lin-xin.gitee.io/images/weixin.jpg) ## Preface The scheme as a set of multi-function background frame templates, suitable for most of the WEB management system development. Convenient development fast simple good components based on Vue3 and ElementPlus. Color separation of color style, support manual switch themes, and it is convenient to use a custom theme color. ## Function - [x] Element-UI - [x] Login/Logout - [x] Dashboard - [x] Table - [x] Tabs - [x] From - [x] Chart :bar_chart: - [x] Editor - [x] Markdown - [x] Upload pictures by clipping or dragging - [x] Permission - [x] Three level menu - [x] Custom icon ## Installation steps git clone https://github.com/lin-xin/vue-manage-system.git // Clone templates cd vue-manage-system // Enter template directory npm install // Installation dependency ## Local development npm run dev ## Constructing production // Constructing project npm run build ## Component description and presentation ### vue-schart Vue.js wrapper for sChart.js. Github : [vue-schart](https://github.com/lin-xin/vue-schart#/) ```html ``` ## Screenshot ### Default theme ![Image text](https://github.com/lin-xin/manage-system/raw/master/screenshots/wms1.png) ### Login ![Image text](https://github.com/lin-xin/manage-system/raw/master/screenshots/wms3.png) ## License [MIT](https://github.com/lin-xin/vue-manage-system/blob/master/LICENSE) ================================================ FILE: vue-manage-system/auto-imports.d.ts ================================================ // Generated by 'unplugin-auto-import' export {} declare global { } ================================================ FILE: vue-manage-system/components.d.ts ================================================ // generated by unplugin-vue-components // We suggest you to commit this file into source control // Read more: https://github.com/vuejs/core/pull/3399 import '@vue/runtime-core' export {} declare module '@vue/runtime-core' { export interface GlobalComponents { ElAvatar: typeof import('element-plus/es')['ElAvatar'] ElButton: typeof import('element-plus/es')['ElButton'] ElCard: typeof import('element-plus/es')['ElCard'] ElCheckbox: typeof import('element-plus/es')['ElCheckbox'] ElCol: typeof import('element-plus/es')['ElCol'] ElDialog: typeof import('element-plus/es')['ElDialog'] ElDropdown: typeof import('element-plus/es')['ElDropdown'] ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem'] ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu'] ElForm: typeof import('element-plus/es')['ElForm'] ElFormItem: typeof import('element-plus/es')['ElFormItem'] ElIcon: typeof import('element-plus/es')['ElIcon'] ElImage: typeof import('element-plus/es')['ElImage'] ElInput: typeof import('element-plus/es')['ElInput'] ElMenu: typeof import('element-plus/es')['ElMenu'] ElMenuItem: typeof import('element-plus/es')['ElMenuItem'] ElOption: typeof import('element-plus/es')['ElOption'] ElPagination: typeof import('element-plus/es')['ElPagination'] ElProgress: typeof import('element-plus/es')['ElProgress'] ElRadio: typeof import('element-plus/es')['ElRadio'] ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup'] ElRow: typeof import('element-plus/es')['ElRow'] ElSelect: typeof import('element-plus/es')['ElSelect'] ElSubMenu: typeof import('element-plus/es')['ElSubMenu'] ElTable: typeof import('element-plus/es')['ElTable'] ElTableColumn: typeof import('element-plus/es')['ElTableColumn'] ElTag: typeof import('element-plus/es')['ElTag'] ElTooltip: typeof import('element-plus/es')['ElTooltip'] ElTree: typeof import('element-plus/es')['ElTree'] ElUpload: typeof import('element-plus/es')['ElUpload'] Header: typeof import('./src/components/header.vue')['default'] RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] Sidebar: typeof import('./src/components/sidebar.vue')['default'] Tags: typeof import('./src/components/tags.vue')['default'] } } ================================================ FILE: vue-manage-system/index.html ================================================ vue-manage-system
================================================ FILE: vue-manage-system/package.json ================================================ { "name": "vue-manage-system", "version": "5.3.0", "private": true, "scripts": { "dev": "vite", "build": "vue-tsc --noEmit && vite build", "serve": "vite preview" }, "dependencies": { "@element-plus/icons-vue": "^2.0.9", "axios": "^0.27.2", "element-plus": "^2.2.14", "jsencrypt": "^3.3.1", "md-editor-v3": "^2.2.1", "pinia": "^2.0.20", "vue": "^3.2.37", "vue-cropperjs": "^5.0.0", "vue-router": "^4.1.3", "vue-schart": "^2.0.0", "wangeditor": "^4.7.15", "xlsx": "^0.18.5" }, "devDependencies": { "@vitejs/plugin-vue": "^3.0.0", "@vue/compiler-sfc": "^3.1.2", "typescript": "^4.6.4", "unplugin-auto-import": "^0.11.2", "unplugin-vue-components": "^0.22.4", "vite": "^3.0.0", "vite-plugin-vue-setup-extend": "^0.4.0", "vue-tsc": "^0.38.4" }, "browserslist": [ "> 1%", "last 2 versions", "not dead" ] } ================================================ FILE: vue-manage-system/public/table.json ================================================ { "list": [{ "id": 1, "name": "张三", "money": 123, "address": "广东省东莞市长安镇", "state": "成功", "date": "2019-11-1", "thumb": "https://lin-xin.gitee.io/images/post/wms.png" }, { "id": 2, "name": "李四", "money": 456, "address": "广东省广州市白云区", "state": "成功", "date": "2019-10-11", "thumb": "https://lin-xin.gitee.io/images/post/node3.png" }, { "id": 3, "name": "王五", "money": 789, "address": "湖南省长沙市", "state": "失败", "date": "2019-11-11", "thumb": "https://lin-xin.gitee.io/images/post/parcel.png" }, { "id": 4, "name": "赵六", "money": 1011, "address": "福建省厦门市鼓浪屿", "state": "成功", "date": "2019-10-20", "thumb": "https://lin-xin.gitee.io/images/post/notice.png" } ], "pageTotal": 4 } ================================================ FILE: vue-manage-system/src/App.vue ================================================ ================================================ FILE: vue-manage-system/src/api/HomeCarousel.ts ================================================ import request from '@/utils/request' // export function addImageIntoHomeCarousel(reqData, _this) { // $.ajax({ // url: "/api/admin/SpImages/upload", // type: "post", //请求方式 // data: reqData, //请求数据 // processData: false, //是否将请求数据转换为对象 // contentType: false, //发送数据到服务器时所使用的内容类型,false表示不需要ajax处理 // dataType: 'json', // success: function(result) { // if (result.code == Code.OK) { // return result.data; // } else { // _this.$global.popupHint(result.msg); // return undefined; // } // }, // error: function() { // _this.$global.popupHint("提交失败"); // return 'ssss'; // } // }); // } export function add(formData) { const config : any = { 'Content-type': 'multipart/form-data' }; return request.post("/api/admin/homeCarousels", formData, config); } export function deleteById(id) { return request.delete("/api/admin/homeCarousels/"+id); } export function modifyDescription(params) { let config = { params: params }; return request.put("/api/admin/homeCarousels/descriptionAndLink", {}, config); } export function coverImage(reqData) { return request.post("/api/admin/homeCarousels/cover", reqData); } export function queryCurrent() { return request.get("/api/admin/homeCarousels/current"); } ================================================ FILE: vue-manage-system/src/api/UserRole.ts ================================================ import request from '@/utils/request' //查 export function addRoleForUser(userId, roleId) { let params = { userId: userId, roleId: roleId }; let config = { params: params } return request.post("/api/admin/userRoles/operate", {}, config); } export function deleteRoleForUser(userId, roleId) { let params = { userId: userId, roleId: roleId }; let config = { params: params } return request.delete("/api/admin/userRoles/operate/"+userId+'/'+roleId); } //操作 ================================================ FILE: vue-manage-system/src/api/WebsiteData.ts ================================================ import request from '@/utils/request' export function queryWebsiteData() { return request.get("/api/admin/websites/accessData"); } ================================================ FILE: vue-manage-system/src/api/api.ts ================================================ import request from '@/utils/request' export function searchApis(params) { return request.get("/api/admin/apis/query/search",{params:params}); } export function addApi(data) { return request.post("/api/admin/apis/operate",data); } export function modifyApi(data) { return request.put("/api/admin/apis/operate",data); } export function deleteApi(id) { return request.delete("/api/admin/apis/operate/"+id); } ================================================ FILE: vue-manage-system/src/api/authorize.ts ================================================ import request from '@/utils/request' //查 export function queryRoleAuthorize(roleId) { return request.get("/api/admin/authorizes/roleId/"+roleId); } //写 export function addAuthorize(formData) { let config={params:formData} return request.post("/api/admin/authorizes",formData,config); } export function deleteAuthorize(roleId,permissionId) { return request.delete("/api/admin/authorizes/"+roleId+'/'+permissionId); } ================================================ FILE: vue-manage-system/src/api/category.ts ================================================ import request from '@/utils/request' //查 export function queryAllCategories() { return request.get("/api/admin/categories/all"); } ================================================ FILE: vue-manage-system/src/api/comment.ts ================================================ import request from '@/utils/request' //查 export function queryCommentsBy(params) { return request.get("/api/admin/comments/query/by", { params: params }); } //操作 export function deleteComment(commentId) { return request.delete("/api/admin/comments/operate/" + commentId); } ================================================ FILE: vue-manage-system/src/api/index.ts ================================================ import request from '../utils/requestx'; export const fetchData = () => { return request({ url: './table.json', method: 'get' }); }; ================================================ FILE: vue-manage-system/src/api/login.ts ================================================ import request from '@/utils/request' export function getPublicKey(){ return request.get("/api/admin/logins/publicKey"); } export function doLogin(data){ return request.post("/api/admin/logins/doLogin",data); } ================================================ FILE: vue-manage-system/src/api/permission.ts ================================================ import request from '@/utils/request' //查 export function queryPermissionTree() { return request.get("/api/admin/permissions/tree"); } export function pagePermission(pageNo,pageSize) { return request.get("/api/admin/permissions/page/"+pageNo+'/'+pageSize); } export function queryModules() { return request.get("/api/admin/permissions/modules"); } export function queryNonModules() { return request.get("/api/admin/permissions/nonModules"); } //写 export function addPermission(data) { return request.post("/api/admin/permissions",data); } export function modifyPermission(data) { return request.put("/api/admin/permissions",data); } export function deletePermission(id) { return request.delete("/api/admin/permissions/"+id); } ================================================ FILE: vue-manage-system/src/api/role.ts ================================================ import request from '@/utils/request' export function queryAllRoles() { return request.get("/api/admin/roles/all"); } export function addRole(data) { return request.post("/api/admin/roles", data); } export function deleteRole(id) { return request.delete("/api/admin/roles/"+id); } export function modifyRole(data) { return request.put("/api/admin/roles", data); } ================================================ FILE: vue-manage-system/src/api/topic.ts ================================================ import request from '@/utils/request' //查 export function queryTopicsOrderBy(params) { return request.get("/api/admin/topics/query/orderBy", { params: params }); } //操作 export function deleteTopic(topicId) { return request.delete("/api/admin/topics/operate/" + topicId); } ================================================ FILE: vue-manage-system/src/api/user.ts ================================================ import request from '@/utils/request' //查 export function queryUsers(params) { return request.get("/api/admin/users/query/search", { params: params }); } //操作 ================================================ FILE: vue-manage-system/src/assets/css/color-dark.css ================================================ .header{ background-color: #242f42; } .login-wrap{ background: #324157; } .plugins-tips{ background: #eef1f6; } .plugins-tips a{ color: #20a0ff; } .tags-li.active { border: 1px solid #409EFF; background-color: #409EFF; } .message-title{ color: #20a0ff; } .collapse-btn:hover{ background: rgb(40,52,70); } ================================================ FILE: vue-manage-system/src/assets/css/icon.css ================================================ [class*=" el-icon-lx"], [class^=el-icon-lx] { font-family: lx-iconfont !important; } ================================================ FILE: vue-manage-system/src/assets/css/main.css ================================================ * { margin: 0; padding: 0; } html, body, #app, .wrapper { width: 100%; height: 100%; overflow: hidden; } body { font-family: 'PingFang SC', "Helvetica Neue", Helvetica, "microsoft yahei", arial, STHeiTi, sans-serif; } a { text-decoration: none } .content-box { position: absolute; left: 250px; right: 0; top: 70px; bottom: 0; padding-bottom: 30px; -webkit-transition: left .3s ease-in-out; transition: left .3s ease-in-out; background: #f0f0f0; } .content { width: auto; height: 100%; padding: 10px; overflow-y: scroll; box-sizing: border-box; } .content-collapse { left: 65px; } .container { padding: 30px; background: #fff; border: 1px solid #ddd; border-radius: 5px; } .crumbs { margin: 10px 0; } .el-table th { background-color: #f5f7fa !important; } .pagination { margin: 20px 0; text-align: right; } .plugins-tips { padding: 20px 10px; margin-bottom: 20px; } .el-button+.el-tooltip { margin-left: 10px; } .el-table tr:hover { background: #f6faff; } .mgb20 { margin-bottom: 20px; } .move-enter-active, .move-leave-active { transition: opacity .1s ease; } .move-enter-from, .move-leave-to { opacity: 0; } /*BaseForm*/ .form-box { width: 600px; } .form-box .line { text-align: center; } .el-time-panel__content::after, .el-time-panel__content::before { margin-top: -7px; } .el-time-spinner__wrapper .el-scrollbar__wrap:not(.el-scrollbar__wrap--hidden-default) { padding-bottom: 0; } [class*=" el-icon-"], [class^=el-icon-] { speak: none; font-style: normal; font-weight: 400; font-variant: normal; text-transform: none; line-height: 1; vertical-align: baseline; display: inline-block; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } .el-sub-menu [class^=el-icon-] { vertical-align: middle; margin-right: 5px; width: 24px; text-align: center; font-size: 18px; } [hidden]{ display: none !important; } ================================================ FILE: vue-manage-system/src/components/header.vue ================================================ ================================================ FILE: vue-manage-system/src/components/sidebar.vue ================================================ ================================================ FILE: vue-manage-system/src/components/tags.vue ================================================ ================================================ FILE: vue-manage-system/src/config.ts ================================================ export default{ domainOfImages:'http://rof8epeiz.hn-bkt.clouddn.com/', } ================================================ FILE: vue-manage-system/src/main.ts ================================================ import { createApp } from 'vue'; import { createPinia } from 'pinia'; import * as ElementPlusIconsVue from '@element-plus/icons-vue'; import App from './App.vue'; import router from './router'; import { usePermissStore } from './store/permiss'; import 'element-plus/dist/index.css'; import './assets/css/icon.css'; const app = createApp(App); app.use(createPinia()); app.use(router); // 注册elementplus图标 for (const [key, component] of Object.entries(ElementPlusIconsVue)) { app.component(key, component); } // 自定义权限指令 const permiss = usePermissStore(); app.directive('permiss', { mounted(el, binding) { if (!permiss.key.includes(String(binding.value))) { el['hidden'] = true; } }, }); app.mount('#app'); ================================================ FILE: vue-manage-system/src/router/index.ts ================================================ import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'; import { usePermissStore } from '../store/permiss'; import CommonUtils from '@/utils/CommonUtils' import Home from '../views/home.vue'; import Table from '../views/table.vue' import HomeCarousel from '../views/HomeCarousel/HomeCarousel.vue' const routes: RouteRecordRaw[] = [ { path: '/', redirect: '/dashboard', }, { path: '/', name: 'Home', component: Home, children: [ { path: '/dashboard', name: 'dashboard', meta: { title: '系统首页', permiss: '1', }, component: () => import(/* webpackChunkName: "dashboard" */ '../views/dashboard.vue'), }, { path: '/HomeCarousel', name: 'HomeCarousel', meta: { title: '走马灯', permiss: '2', }, component: () => import(/* webpackChunkName: "HomeCarousel" */ '../views/HomeCarousel/HomeCarousel.vue'), }, { path: '/role', name: 'role', meta: { title: '角色', permiss: '5', }, component: () => import(/* webpackChunkName: "role" */ '../views/role/role.vue'), }, { path: '/permission/:pageNo', name: 'permission', meta: { title: '权限', permiss: '1', }, component: () => import(/* webpackChunkName: "permission" */ '../views/permission/permission.vue'), }, { path: '/authorize', name: 'authorize', meta: { title: '授权', permiss: '1', }, component: () => import(/* webpackChunkName: "authorize" */ '../views/authorize/authorize.vue'), }, { path: '/api', name: 'api', meta: { title: '接口', permiss: '1', }, component: () => import(/* webpackChunkName: "api" */ '../views/api/api.vue'), }, { path: '/topic', name: 'topic', meta: { title: '话题', permiss: '1', }, component: () => import(/* webpackChunkName: "topic" */ '../views/topic/topic.vue'), }, { path: '/user', name: 'user', meta: { title: '用户', permiss: '1', }, component: () => import(/* webpackChunkName: "user" */ '../views/user/user.vue'), }, { path: '/comment', name: 'comment', meta: { title: '评论', permiss: '1', }, component: () => import(/* webpackChunkName: "comment" */ '../views/comment/comment.vue'), }, ], }, { path: '/login', name: 'Login', meta: { title: '登录', }, component: () => import(/* webpackChunkName: "login" */ '../views/login.vue'), }, { path: '/403', name: '403', meta: { title: '没有权限', }, component: () => import(/* webpackChunkName: "403" */ '../views/403.vue'), }, ]; const router = createRouter({ history: createWebHashHistory(), routes, }); router.beforeEach((to, from, next) => { document.title = `${to.meta.title} | vue-manage-system`; const token = localStorage.getItem('token'); if (!!CommonUtils.isEmpty(token) && to.path !== '/login') { next('/login'); } /*else if (to.meta.permiss && !permiss.key.includes(to.meta.permiss)) { // 如果没有权限,则进入403 next('/403'); }*/ else { next(); } }); export default router; ================================================ FILE: vue-manage-system/src/store/permiss.ts ================================================ import { defineStore } from 'pinia'; interface ObjectList { [key: string]: string[]; } export const usePermissStore = defineStore('permiss', { state: () => { const keys = localStorage.getItem('ms_keys'); // const keys = localStorage.getItem('token'); return { key: keys ? JSON.parse(keys) : [], defaultList: { admin: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', '16'], user: ['1', '2', '3', '11', '13', '14', '15'] } }; }, actions: { handleSet(val: string[]) { this.key = val; } } }); ================================================ FILE: vue-manage-system/src/store/sidebar.ts ================================================ import { defineStore } from 'pinia'; export const useSidebarStore = defineStore('sidebar', { state: () => { return { collapse: true }; }, getters: {}, actions: { handleCollapse() { this.collapse = !this.collapse; } } }); ================================================ FILE: vue-manage-system/src/store/store.ts ================================================ import { defineStore } from 'pinia'; import { queryAllCategories } from '@/api/category' import { queryAllRoles } from '@/api/role'; import { Code } from '@/utils/result' interface Category { id: number; label: string; createTime: string; updateTime: string; } import { Role } from '@/views/role/role.vue' export const useStore = defineStore('store', { state: () => { return { categoryList: [], roleList: [], token:'' }; }, getters: { categoryLabel(state) { return (id) => { for (let item of state.categoryList) { if (item.id == id) { return item.label; } } return null; } }, roleName(state) { return (id) => { for (let item of state.roleList) { if (item.id == id) { return item.roleName; } } return null; } }, }, actions: { init() { let _this = this; if (this.categoryList.length == 0) { queryAllCategories().then((res: any) => { if (res.code == Code.OK) { _this.categoryList = res.data; } }); } if (this.roleList.length == 0) { queryAllRoles().then((res: any) => { if (res.code == Code.OK) { this.roleList = res.data; } }) } }, setToken(token){ localStorage.setItem('token', token); }, removeToken(){ localStorage.removeItem('token') } } }); ================================================ FILE: vue-manage-system/src/store/tags.ts ================================================ import { defineStore } from 'pinia'; interface ListItem { name: string; path: string; title: string; } export const useTagsStore = defineStore('tags', { state: () => { return { list: [] }; }, getters: { show: state => { return state.list.length > 0; }, nameList: state => { return state.list.map(item => item.name); } }, actions: { delTagsItem(index: number) { this.list.splice(index, 1); }, setTagsItem(data: ListItem) { this.list.push(data); }, clearTags() { this.list = []; }, closeTagsOther(data: ListItem[]) { this.list = data; }, closeCurrentTag(data: any) { for (let i = 0, len = this.list.length; i < len; i++) { const item = this.list[i]; if (item.path === data.$route.fullPath) { if (i < len - 1) { data.$router.push(this.list[i + 1].path); } else if (i > 0) { data.$router.push(this.list[i - 1].path); } else { data.$router.push('/'); } this.list.splice(i, 1); break; } } } } }); ================================================ FILE: vue-manage-system/src/utils/CommonUtils.ts ================================================ import MessageUtils from '@/utils/MessageUtils' import { Code } from "@/utils/result" export default { isEmpty(object) { if (object == undefined || object == '' || object == null || object == {} || object == []) { return true; } return false; }, popMsgAndRefreshIfOk(request, msg, delaySeconds: void) { request.then(result => { console.log(result); if (result.code == Code.OK) { MessageUtils.success(msg, 1); if (!this.isEmpty(delaySeconds)) { this.delayRefresh(delaySeconds); } } }); }, popMsgIfOk(request, msg) { this.popMsgAndRefreshIfOk(request, msg); }, delayRefresh(delaySeconds) { setTimeout(() => { location.reload(); }, delaySeconds * 1000); }, getUrlParams(url) { let firstIndex = url.indexOf('?'); if (firstIndex == -1) return; let argStr = url.slice(firstIndex + 1, url.length); let args = argStr.split("&"); let params = {}; for (let i = 0; i < args.length; i++) { let keyAndValues = args[i].split("="); params[keyAndValues[0]] = keyAndValues[1]; } return params; }, } ================================================ FILE: vue-manage-system/src/utils/MessageUtils.ts ================================================ import { ElMessageBox, ElNotification } from 'element-plus'; export default{ notice(msg,durationSeconds){ let newDuration=durationSeconds||2; ElNotification({ title: '提示', message: msg, type: 'warning', duration: newDuration*1000 }); }, success(msg,durationSeconds){ let newDuration=durationSeconds||4; ElNotification({ title: '成功', message: msg, type: 'success', duration: newDuration*1000 }); }, confirm(msg){ return ElMessageBox.confirm(msg, '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }) }, } ================================================ FILE: vue-manage-system/src/utils/StringUtils.ts ================================================ export default { html2Text(htmlString) { return htmlString.toString().replace(/<(style|script|iframe)[^>]*?>[\s\S]+?<\/\1\s*>/gi, '') .replace(/<[^>]+?>/g, '') .replace(/\s+/g, ' ') .replace(/ /g, ' ') .replace(/>/g, ' ') .replace(/ /gm,' '); } } ================================================ FILE: vue-manage-system/src/utils/global.ts ================================================ import Vue from 'vue' import CommonUtils from '@/utils/CommonUtils' import MessageUtils from '@/utils/MessageUtils' import Config from '@/config' import { Code } from '@/utils/result' import StringUtils from '@/utils/StringUtils' let global: any = {}; //消息提示功能 // global.popupHint = function(msg) { // this.$notify({ // title: '提示', // message: msg, // type: 'warning' // }); // } // global.popupSuccess = function(msg) { // this.$notify({ // title: '成功', // message: msg, // type: 'success' // }); // } // global.popupMsgIfOk = function(request, msg) { // request.then(result => { // console.log(result); // if (result.code == Code.OK) { // global.popupSuccess(msg); // } // }); // } // global.openConfirm = function(msg) { // return MessageBox.confirm(msg, '提示', { // confirmButtonText: '确定', // cancelButtonText: '取消', // type: 'warning' // }) // } //真实图片链接 global.baseImageUrl = Config.domainOfImages; global.trueImageUrl = function(url) { if (CommonUtils.isEmpty(url) || url == '#') { return '#'; } return global.baseImageUrl + url; } //获取头像路径 global.truePhotoUrl = function(url) { if (CommonUtils.isEmpty(url) || url == '#') { return global.baseImageUrl + 'userPhoto/default.jpeg'; } return global.baseImageUrl + url; } //获取分享链接 global.getTopicUrl = function(topicId: number) { return '/topic/' + topicId; } // //token及token内信息 // global.TOKEN = function() { // let token = localStorage.getItem("token"); // if (CommonUtils.isEmpty(token)) { // return ""; // } else { // return token; // } // } // global.setToken = function(token) { // localStorage.setItem("token", token); // } // global.removeToken = function() { // localStorage.removeItem("token"); // } // global.PHOTO_URL = function() { // let token = global.TOKEN(); // if (!CommonUtils.isEmpty(token)) { // return global.truePhotoUrl(jwtDecode(token).photoUrl); // } // return '#'; // } // global.USERNAME = function() { // let token = global.TOKEN(); // if (!CommonUtils.isEmpty(token)) { // return jwtDecode(token).username; // } // return ''; // } // global.USER_ID = function() { // let token = global.TOKEN(); // if (!CommonUtils.isEmpty(token)) { // return jwtDecode(token).userId; // } // return ''; // } //限制显示长度 global.omitStr = function(content, totalLength) { if (content.length > totalLength) { return content.slice(0, totalLength - 3) + '...' } else { return content; } } global.html2Text = function(contentHtml, limitLength) { let content = StringUtils.html2Text(contentHtml); if (limitLength == undefined) { return content; } if (content.length > limitLength) { return content.slice(0, limitLength - 3) + '...' } else { return content; } } export default global; ================================================ FILE: vue-manage-system/src/utils/request.ts ================================================ import axios from 'axios'; import { ElMessage, ElMessageBox, ElLoading, ElNotification } from 'element-plus'; import MessageUtils from '@/utils/MessageUtils' import { Code } from '@/utils/result'; // const service = axios.create({ // baseURL: "http://127.0.0.1:8080/projectName",//请求地址前缀 // timeout: 0 // }); const service = axios.create(); var requestNum = 0; var loading: any = null; // 请求拦截器 service.interceptors.request.use( (config: any) => { //添加请求头部参数 // config.headers['arg1'] = "arg1Value"; //开始loading config.headers['authorization'] = localStorage.getItem('token'); requestNum++; if (loading == null) { loading = ElLoading.service({ fullscreen: true, text: '正在努力加载中~' }); } else if (loading != null && requestNum > 0) { loading = ElLoading.service({ fullscreen: true, text: '正在努力加载中~' }); } return config; }, error => { requestNum = 0; if (loading) { loading.close(); } return Promise.reject(error); } ); // 响应拦截器 service.interceptors.response.use( response => { //拦截到成功的数据 requestNum--; if (loading == null || requestNum <= 0) { loading.close() } const result = response.data; if (result.code == Code.ERR) { MessageUtils.notice(result.msg, 2) } else if (result.code == Code.OK) { return response.data; } else { // 出错了直接关闭loading requestNum = 0 loading.close(); } }, error => { //拦截到失败的数据 console.log('错误码', error) // 出错了直接关闭loading requestNum = 0 loading.close(); MessageUtils.notice(error, 4) return Promise.reject(error); } ); export default service; ================================================ FILE: vue-manage-system/src/utils/requestx.ts ================================================ import axios, {AxiosInstance, AxiosError, AxiosResponse, AxiosRequestConfig} from 'axios'; const service:AxiosInstance = axios.create({ timeout: 5000 }); service.interceptors.request.use( (config: AxiosRequestConfig) => { return config; }, (error: AxiosError) => { console.log(error); return Promise.reject(); } ); service.interceptors.response.use( (response: AxiosResponse) => { if (response.status === 200) { return response; } else { Promise.reject(); } }, (error: AxiosError) => { console.log(error); return Promise.reject(); } ); export default service; ================================================ FILE: vue-manage-system/src/utils/result.ts ================================================ // 状态码 export let Code= { OK: 20000, ERR: 20001, DATA_NOT_EXIST: 20011 } // //获取图片路径 // Vue.prototype.getImageUrl = function(imageId) { // return "../storage/images/" + imageId + ".jpeg"; // } // //获取头像路径 // Vue.prototype.getPhotoUrl = function(photoId) { // return "../storage/photos/" + photoId + ".jpeg"; // } // //获取分享链接 // Vue.prototype.getTopicUrl = function(shareId) { // return '/templates/ShowTopic.html?id=' + shareId // } // /*获取存储在浏览器cookie的用户信息*/ // Vue.prototype.PHOTO_URL = function() { // let photoId = $.cookie('photoId'); // if (!isEmpty(photoId)) { // return this.getPhotoUrl(photoId); // } // return '#'; // } // Vue.prototype.USERNAME = function() { // let username = $.cookie('username'); // if (!isEmpty(username)) { // return username; // } // return ''; // } // Vue.prototype.USER_ID = function() { // let userId = $.cookie('userId'); // if (!isEmpty(userId)) { // return userId; // } // return ''; // } // /*End cookie信息*/ // /*消息提示,进一步封装elementUI的消息提示*/ // Vue.prototype.popupHint = function(msg) { // this.$notify({ // title: '提示', // message: msg, // type: 'warning' // }); // } // Vue.prototype.popupSuccess = function(msg) { // this.$notify({ // title: '成功', // message: msg, // type: 'success' // }); // } //控制显示长度 // Vue.prototype.omitStr = function(content, totalLength) { // if (content.length > totalLength) { // return content.slice(0, totalLength - 3) + '...' // } else { // return content; // } // } //axios拦截 // axios.interceptors.response.use( // function(response) { // let result = response.data; // if (result.code == Code.ERR) { // Vue.prototype.popupHint(result.msg); // return false; // } // return response; // }, // function(error) { // // 对响应错误进行操作 // return Promise.reject(error); // } // ); ================================================ FILE: vue-manage-system/src/utils/utils.js ================================================ ================================================ FILE: vue-manage-system/src/views/403.vue ================================================ ================================================ FILE: vue-manage-system/src/views/404.vue ================================================ ================================================ FILE: vue-manage-system/src/views/HomeCarousel/HomeCarousel.vue ================================================ ================================================ FILE: vue-manage-system/src/views/api/api.vue ================================================ ================================================ FILE: vue-manage-system/src/views/authorize/authorize.vue ================================================ ================================================ FILE: vue-manage-system/src/views/charts.vue ================================================ ================================================ FILE: vue-manage-system/src/views/comment/comment.vue ================================================ ================================================ FILE: vue-manage-system/src/views/dashboard.vue ================================================ ================================================ FILE: vue-manage-system/src/views/donate.vue ================================================ ================================================ FILE: vue-manage-system/src/views/editor.vue ================================================ ================================================ FILE: vue-manage-system/src/views/home.vue ================================================ ================================================ FILE: vue-manage-system/src/views/icon.vue ================================================ ================================================ FILE: vue-manage-system/src/views/login.vue ================================================ ================================================ FILE: vue-manage-system/src/views/markdown.vue ================================================ ================================================ FILE: vue-manage-system/src/views/permission/permission.vue ================================================ ================================================ FILE: vue-manage-system/src/views/permissionx.vue ================================================ ================================================ FILE: vue-manage-system/src/views/role/role.vue ================================================ ================================================ FILE: vue-manage-system/src/views/table.vue ================================================ ================================================ FILE: vue-manage-system/src/views/tabs.vue ================================================ ================================================ FILE: vue-manage-system/src/views/topic/topic.vue ================================================ ================================================ FILE: vue-manage-system/src/views/upload.vue ================================================ ================================================ FILE: vue-manage-system/src/views/user/user.vue ================================================ ================================================ FILE: vue-manage-system/src/views/user.vue ================================================ ================================================ FILE: vue-manage-system/src/vite-env.d.ts ================================================ /// declare module '*.vue' { import type { DefineComponent } from 'vue' const component: DefineComponent<{}, {}, any> export default component } declare module 'vue-schart'; declare module 'vue-cropperjs'; ================================================ FILE: vue-manage-system/tsconfig.json ================================================ { "compilerOptions": { "target": "ESNext", "useDefineForClassFields": true, "module": "ESNext", "moduleResolution": "Node", "strict": true, "jsx": "preserve", "sourceMap": true, "resolveJsonModule": true, "isolatedModules": true, "esModuleInterop": true, "lib": ["ESNext", "DOM"], "skipLibCheck": true, "baseUrl": ".", "noImplicitAny": false, "paths": { "@/*": ["src/*"] } }, "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.vue"], "references": [{ "path": "./tsconfig.node.json" }] } ================================================ FILE: vue-manage-system/tsconfig.node.json ================================================ { "compilerOptions": { "composite": true, "module": "ESNext", "moduleResolution": "Node", "allowSyntheticDefaultImports": true }, "include": ["vite.config.ts"] } ================================================ FILE: vue-manage-system/vite.config.ts ================================================ import { defineConfig } from 'vite'; import vue from '@vitejs/plugin-vue'; import VueSetupExtend from 'vite-plugin-vue-setup-extend'; import AutoImport from 'unplugin-auto-import/vite'; import Components from 'unplugin-vue-components/vite'; import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'; import { resolve } from 'path' export default defineConfig({ base: './', plugins: [ vue(), VueSetupExtend(), AutoImport({ resolvers: [ElementPlusResolver()] }), Components({ resolvers: [ElementPlusResolver()] }) ], resolve: { alias: { '@': resolve('src') } }, optimizeDeps: { include: ['schart.js'] }, server: { port: 7010, proxy: { "/api": { // 代理名称 凡是使用/api开头的地址都是用此代理 target: "http://127.0.0.1:81/", // 需要代理访问的api地址 changeOrigin: true, // 允许跨域请求 // pathRewrite: { // // 重写路径,替换请求地址中的指定路径 // "^/api": "/", // 将请求地址中的/api替换为空,也就是请求地址中不会包含/api/ // }, }, "/acfile": { // 代理名称 凡是使用/api开头的地址都是用此代理 target: "http://127.0.0.1:81/", // 需要代理访问的api地址 changeOrigin: true, // 允许跨域请求 // pathRewrite: { // // 重写路径,替换请求地址中的指定路径 // "^/api": "/", // 将请求地址中的/api替换为空,也就是请求地址中不会包含/api/ // }, }, }, } }); ================================================ FILE: vue_acimage_web/.gitignore ================================================ .DS_Store node_modules /dist # local env files .env.local .env.*.local # Log files npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* # Editor directories and files .idea .vscode *.suo *.ntvs* *.njsproj *.sln *.sw? ================================================ FILE: vue_acimage_web/README.md ================================================ # vue_acimage_web > acimage网页端 ## clone仓库 ``` git clone https://github.com/ggggborn/SpringCloud-acimage.git ``` ## 进入到vue_acimage_web目录 ``` cd vue_acimage_web ``` ## 安装依赖 ``` npm install -i ``` ## 运行 ``` npm run serve ``` ================================================ FILE: vue_acimage_web/babel.config.js ================================================ module.exports = { presets: [ '@vue/cli-plugin-babel/preset' ] } ================================================ FILE: vue_acimage_web/jsconfig.json ================================================ { "compilerOptions": { "target": "es5", "module": "esnext", "baseUrl": "./", "moduleResolution": "node", "paths": { "@/*": [ "src/*" ] }, "lib": [ "esnext", "dom", "dom.iterable", "scripthost" ] } } ================================================ FILE: vue_acimage_web/mock/HomeCarousel.js ================================================ import Mock from 'mockjs' import { Code } from '@/utils/result.js' Mock.mock('/api/image/homeCarousels/all', 'get', { code: Code.OK, 'data|2': [{ id: '@id()', description: '@cparagraph()', url: '/test/test1.jpeg', }, { id: '@id()', description: '@cparagraph()', url: '/test/test2.jpeg', } ] }) ================================================ FILE: vue_acimage_web/mock/UserRank.js ================================================ import Mock from 'mockjs' import { Code } from '@/utils/result.js' Mock.mock('/api/community/users/rank/starCount/1', 'get', { code: Code.OK, 'data|4': [{ id: '@id', username: '@cname()', photoUrl: '', starCount: '@integer(0, 10000)', topicCount: '@integer(0, 10000)', }] }) Mock.mock('/api/community/users/rank/topicCount/1', 'get', { code: Code.OK, 'data|5': [{ id: '@id', username: '@cname()', photoUrl: '', starCount: '@integer(0, 10000)', topicCount: '@integer(0, 10000)', }] }) ================================================ FILE: vue_acimage_web/mock/index.js ================================================ import Mock from 'mockjs' require('./topic.js') require('./HomeCarousel.js') require('./UserRank.js') ================================================ FILE: vue_acimage_web/mock/topic.js ================================================ import Mock from 'mockjs' import { Code } from '@/utils/result.js' Mock.mock('/api/community/topics/recentHot', 'get', { code: Code.OK, 'data|4': [{ id: '@id()', 'title|5': '@cname()', starCount: 888, pageView: 88888, createTime: '@date()', user: { username: '@cname()', photoUrl: '' }, coverImageUrl: 'test/test1.jpeg', categoryId:1, }, { id: '@id()', 'title|15': '@cname()', starCount: 888, pageView: 88888, createTime: '2022-2-22 22:22:22', user: { username: '@cname()', photoUrl: '' }, coverImageUrl: 'topicImage/2022/12/02/1573240094587424768', categoryId:1, }, ] }) Mock.mock('/api/community/topics/recommend', 'get', { code: Code.OK, 'data|2': [{ id: '@integer()', 'title|5': '@cname()', commentCount:123, starCount: 888, pageView: 88888, createTime: '@date()', user: { username: '@cname()', photoUrl: '' }, coverImageUrl: 'test/test1.jpeg', }, { id: '@integer()', 'title|15': '@cname()', commentCount:123, starCount: 888, pageView: 88888, createTime: '2022-2-22 22:22:22', user: { username: '刚刚刚刚刚刚刚刚', photoUrl: '' }, coverImageUrl: 'topicImage/2022/12/02/1573240094587424768', categoryId:1, }, ] }) Mock.mock('/api/community/topics/pageRecentTopics/1', 'get', { code: Code.OK, data: { 'dataList|5': [{ id: '@integer()', userId: 999, 'title|10': "@cname()", 'content': `不知道@cparagraph()`, activityTime: '2022-2-22 2:22:22', starCount: 666, pageView: 777, commentCount: 888, coverImageUrl: 'static/image/user-rank-header.jpg', categoryId:1, images: [{ id: 0 }], user: { photoUrl: '', 'username|2': "@cname()" } }, ], totalCount: 10, } }) ================================================ FILE: vue_acimage_web/package.json ================================================ { "name": "vue_acimage_web", "version": "0.1.0", "private": true, "scripts": { "serve": "vue-cli-service serve", "dev": "vue-cli-service serve --mode dev", "build": "vue-cli-service build", "lint": "vue-cli-service lint" }, "dependencies": { "@tinymce/tinymce-vue": "^3.2.8", "axios": "^1.2.0", "core-js": "^3.8.3", "element-ui": "^2.15.12", "jsencrypt": "^3.3.1", "jwt-decode": "^3.1.2", "mockjs": "^1.1.0", "vue": "^2.6.14", "vue-cookie": "^1.1.4", "vue-cropper": "^0.5.8", "vue-dompurify-html": "^3.1.2", "vue-router": "^3.6.5", "vuex": "^3.6.2" }, "devDependencies": { "@babel/core": "^7.12.16", "@babel/eslint-parser": "^7.12.16", "@vue/cli-plugin-babel": "~5.0.0", "@vue/cli-plugin-eslint": "~5.0.0", "@vue/cli-service": "~5.0.0", "eslint": "^7.32.0", "eslint-plugin-vue": "^8.0.3", "vue-template-compiler": "^2.6.14" }, "eslintConfig": { "root": true, "env": { "node": true }, "extends": [ "plugin:vue/essential", "eslint:recommended" ], "parserOptions": { "parser": "@babel/eslint-parser" }, "rules": { "no-unused-vars": "off" } }, "browserslist": [ "> 1%", "last 2 versions", "not dead" ] } ================================================ FILE: vue_acimage_web/public/index.html ================================================ 次元印象o(*≧▽≦)ツ~动漫交流论坛
================================================ FILE: vue_acimage_web/public/static/css/common.css ================================================ body { margin: 0px; padding: 0px; } /* 隐藏上传图片时的+号 */ .upload-plus-disabled .el-upload--picture-card { display: none; } .no-underline { text-decoration: none; } /* 改变router-link颜色变化 */ .router-link-item { color: #666666; } .router-link-item:hover { color: #0074A0; } .hover-pointer:hover { cursor: pointer } .hover-shadow:hover{ box-shadow: 0px 0px 15px #E7E6E3 !important; transition: 0.4s; } /* .hover-border-light:hover { border: 1px solid skyblue !important; } */ .c6 { color: #666666; } .c9 { color: #999999; } ================================================ FILE: vue_acimage_web/public/tinymce/langs/README.md ================================================ This is where language files should be placed. Please DO NOT translate these directly use this service: https://www.transifex.com/projects/p/tinymce/ ================================================ FILE: vue_acimage_web/public/tinymce/langs/zh-Hans.js ================================================ /*! * TinyMCE Language Pack * * Copyright (c) 2022 Ephox Corporation DBA Tiny Technologies, Inc. * Licensed under the Tiny commercial license. See https://www.tiny.cloud/legal/ */ tinymce.addI18n('zh-Hans', { "Redo": "重做", "Undo": "撤销", "Cut": "剪切", "Copy": "复制", "Paste": "粘贴", "Select all": "全选", "New document": "新建文档", "Ok": "确定", "Cancel": "取消", "Visual aids": "网格线", "Bold": "粗体", "Italic": "斜体", "Underline": "下划线", "Strikethrough": "删除线", "Superscript": "上标", "Subscript": "下标", "Clear formatting": "清除格式", "Remove": "移除", "Align left": "左对齐", "Align center": "居中对齐", "Align right": "右对齐", "No alignment": "未对齐", "Justify": "两端对齐", "Bullet list": "无序列表", "Numbered list": "有序列表", "Decrease indent": "减少缩进", "Increase indent": "增加缩进", "Close": "关闭", "Formats": "格式", "Your browser doesn't support direct access to the clipboard. Please use the Ctrl+X/C/V keyboard shortcuts instead.": "你的浏览器不支持打开剪贴板,请使用Ctrl+X/C/V等快捷键。", "Headings": "标题", "Heading 1": "一级标题", "Heading 2": "二级标题", "Heading 3": "三级标题", "Heading 4": "四级标题", "Heading 5": "五级标题", "Heading 6": "六级标题", "Preformatted": "预先格式化的", "Div": "Div", "Pre": "前言", "Code": "代码", "Paragraph": "段落", "Blockquote": "引文区块", "Inline": "文本", "Blocks": "样式", "Paste is now in plain text mode. Contents will now be pasted as plain text until you toggle this option off.": "当前为纯文本粘贴模式,再次点击可以回到普通粘贴模式。", "Fonts": "字体", "Font sizes": "字体大小", "Class": "类型", "Browse for an image": "浏览图像", "OR": "或", "Drop an image here": "拖放一张图像至此", "Upload": "上传", "Uploading image": "上传图片", "Block": "块", "Align": "对齐", "Default": "预设", "Circle": "空心圆", "Disc": "实心圆", "Square": "实心方块", "Lower Alpha": "小写英文字母", "Lower Greek": "小写希腊字母", "Lower Roman": "小写罗马数字", "Upper Alpha": "大写英文字母", "Upper Roman": "大写罗马数字", "Anchor...": "锚点...", "Anchor": "锚点", "Name": "名称", "ID": "ID", "ID should start with a letter, followed only by letters, numbers, dashes, dots, colons or underscores.": "ID应该以英文字母开头,后面只能有英文字母、数字、破折号、点、冒号或下划线。", "You have unsaved changes are you sure you want to navigate away?": "你还有文档尚未保存,确定要离开?", "Restore last draft": "恢复上次的草稿", "Special character...": "特殊字符...", "Special Character": "特殊字符", "Source code": "源代码", "Insert/Edit code sample": "插入/编辑代码示例", "Language": "语言", "Code sample...": "示例代码...", "Left to right": "由左到右", "Right to left": "由右到左", "Title": "标题", "Fullscreen": "全屏", "Action": "动作", "Shortcut": "快捷方式", "Help": "帮助", "Address": "地址", "Focus to menubar": "移动焦点到菜单栏", "Focus to toolbar": "移动焦点到工具栏", "Focus to element path": "移动焦点到元素路径", "Focus to contextual toolbar": "移动焦点到上下文菜单", "Insert link (if link plugin activated)": "插入链接 (如果链接插件已激活)", "Save (if save plugin activated)": "保存(如果保存插件已激活)", "Find (if searchreplace plugin activated)": "查找(如果查找替换插件已激活)", "Plugins installed ({0}):": "已安装插件 ({0}):", "Premium plugins:": "优秀插件:", "Learn more...": "了解更多...", "You are using {0}": "你正在使用 {0}", "Plugins": "插件", "Handy Shortcuts": "快捷键", "Horizontal line": "水平分割线", "Insert/edit image": "插入/编辑图片", "Alternative description": "替代描述", "Accessibility": "辅助功能", "Image is decorative": "图像是装饰性的", "Source": "源", "Dimensions": "尺寸", "Constrain proportions": "保持比例", "General": "一般", "Advanced": "高级", "Style": "样式", "Vertical space": "垂直间距", "Horizontal space": "水平间距", "Border": "框线", "Insert image": "插入图片", "Image...": "图片...", "Image list": "图片清单", "Resize": "调整大小", "Insert date/time": "插入日期/时间", "Date/time": "日期/时间", "Insert/edit link": "插入/编辑链接", "Text to display": "要显示的文本", "Url": "地址", "Open link in...": "链接打开位置...", "Current window": "当前窗口", "None": "无", "New window": "新窗口", "Open link": "打开链接", "Remove link": "移除链接", "Anchors": "锚点", "Link...": "链接...", "Paste or type a link": "粘贴或输入链接", "The URL you entered seems to be an email address. Do you want to add the required mailto: prefix?": "你所填写的URL地址为邮件地址,需要加上mailto: 前缀吗?", "The URL you entered seems to be an external link. Do you want to add the required http:// prefix?": "你所填写的URL地址属于外部链接,需要加上http:// 前缀吗?", "The URL you entered seems to be an external link. Do you want to add the required https:// prefix?": "您输入的 URL 似乎是一个外部链接。您想添加所需的 https:// 前缀吗?", "Link list": "链接清单", "Insert video": "插入视频", "Insert/edit video": "插入/编辑视频", "Insert/edit media": "插入/编辑媒体", "Alternative source": "镜像", "Alternative source URL": "替代来源网址", "Media poster (Image URL)": "封面(图片地址)", "Paste your embed code below:": "将内嵌代码粘贴在下面:", "Embed": "内嵌", "Media...": "多媒体...", "Nonbreaking space": "不间断空格", "Page break": "分页符", "Paste as text": "粘贴为文本", "Preview": "预览", "Print": "打印", "Print...": "打印...", "Save": "保存", "Find": "寻找", "Replace with": "替换为", "Replace": "替换", "Replace all": "替换全部", "Previous": "上一个", "Next": "下一个", "Find and Replace": "查找和替换", "Find and replace...": "查找并替换...", "Could not find the specified string.": "未找到搜索内容。", "Match case": "大小写匹配", "Find whole words only": "全字匹配", "Find in selection": "在选区中查找", "Insert table": "插入表格", "Table properties": "表格属性", "Delete table": "删除表格", "Cell": "单元格", "Row": "行", "Column": "栏目", "Cell properties": "单元格属性", "Merge cells": "合并单元格", "Split cell": "拆分单元格", "Insert row before": "在上方插入行", "Insert row after": "在下方插入行", "Delete row": "删除行", "Row properties": "行属性", "Cut row": "剪切行", "Cut column": "剪切列", "Copy row": "复制行", "Copy column": "复制列", "Paste row before": "粘贴行到上方", "Paste column before": "粘贴此列前", "Paste row after": "粘贴行到下方", "Paste column after": "粘贴后面的列", "Insert column before": "在左侧插入列", "Insert column after": "在右侧插入列", "Delete column": "删除列", "Cols": "列", "Rows": "行数", "Width": "宽度", "Height": "高度", "Cell spacing": "单元格外间距", "Cell padding": "单元格内边距", "Row clipboard actions": "行剪贴板操作", "Column clipboard actions": "列剪贴板操作", "Table styles": "表格样式", "Cell styles": "单元格样式", "Column header": "列标题", "Row header": "行头", "Table caption": "表格标题", "Caption": "标题", "Show caption": "显示标题", "Left": "左", "Center": "居中", "Right": "右", "Cell type": "储存格别", "Scope": "范围", "Alignment": "对齐", "Horizontal align": "水平对齐", "Vertical align": "垂直对齐", "Top": "上方对齐", "Middle": "居中对齐", "Bottom": "下方对齐", "Header cell": "表头单元格", "Row group": "行组", "Column group": "列组", "Row type": "行类型", "Header": "表头", "Body": "表体", "Footer": "表尾", "Border color": "框线颜色", "Solid": "实线", "Dotted": "虚线", "Dashed": "虚线", "Double": "双精度", "Groove": "凹槽", "Ridge": "海脊座", "Inset": "嵌入", "Outset": "外置", "Hidden": "隐藏", "Insert template...": "插入模板...", "Templates": "模板", "Template": "模板", "Insert Template": "插入模板", "Text color": "文本颜色", "Background color": "背景颜色", "Custom...": "自定义......", "Custom color": "自定义颜色", "No color": "无", "Remove color": "移除颜色", "Show blocks": "显示区块边框", "Show invisible characters": "显示不可见字符", "Word count": "字数", "Count": "计数", "Document": "文档", "Selection": "选择", "Words": "单词", "Words: {0}": "字数:{0}", "{0} words": "{0} 字", "File": "文件", "Edit": "编辑", "Insert": "插入", "View": "查看", "Format": "格式", "Table": "表格", "Tools": "工具", "Powered by {0}": "由{0}驱动", "Rich Text Area. Press ALT-F9 for menu. Press ALT-F10 for toolbar. Press ALT-0 for help": "编辑区。按ALT-F9打开菜单,按ALT-F10打开工具栏,按ALT-0查看帮助", "Image title": "图片标题", "Border width": "边框宽度", "Border style": "边框样式", "Error": "错误", "Warn": "警告", "Valid": "有效", "To open the popup, press Shift+Enter": "按Shitf+Enter键打开对话框", "Rich Text Area": "富文本区域", "Rich Text Area. Press ALT-0 for help.": "编辑区。按Alt+0键打开帮助。", "System Font": "系统字体", "Failed to upload image: {0}": "图片上传失败: {0}", "Failed to load plugin: {0} from url {1}": "插件加载失败: {0} 来自链接 {1}", "Failed to load plugin url: {0}": "插件加载失败 链接: {0}", "Failed to initialize plugin: {0}": "插件初始化失败: {0}", "example": "示例", "Search": "搜索", "All": "全部", "Currency": "货币", "Text": "文字", "Quotations": "引用", "Mathematical": "数学", "Extended Latin": "拉丁语扩充", "Symbols": "符号", "Arrows": "箭头", "User Defined": "自定义", "dollar sign": "美元符号", "currency sign": "货币符号", "euro-currency sign": "欧元符号", "colon sign": "冒号", "cruzeiro sign": "克鲁赛罗币符号", "french franc sign": "法郎符号", "lira sign": "里拉符号", "mill sign": "密尔符号", "naira sign": "奈拉符号", "peseta sign": "比塞塔符号", "rupee sign": "卢比符号", "won sign": "韩元符号", "new sheqel sign": "新谢克尔符号", "dong sign": "越南盾符号", "kip sign": "老挝基普符号", "tugrik sign": "图格里克符号", "drachma sign": "德拉克马符号", "german penny symbol": "德国便士符号", "peso sign": "比索符号", "guarani sign": "瓜拉尼符号", "austral sign": "澳元符号", "hryvnia sign": "格里夫尼亚符号", "cedi sign": "塞地符号", "livre tournois sign": "里弗弗尔符号", "spesmilo sign": "spesmilo符号", "tenge sign": "坚戈符号", "indian rupee sign": "印度卢比", "turkish lira sign": "土耳其里拉", "nordic mark sign": "北欧马克", "manat sign": "马纳特符号", "ruble sign": "卢布符号", "yen character": "日元字样", "yuan character": "人民币元字样", "yuan character, in hong kong and taiwan": "元字样(港台地区)", "yen/yuan character variant one": "元字样(大写)", "Emojis": "Emojis", "Emojis...": "Emojis...", "Loading emojis...": "正在加载Emojis...", "Could not load emojis": "无法加载Emojis", "People": "人类", "Animals and Nature": "动物和自然", "Food and Drink": "食物和饮品", "Activity": "活动", "Travel and Places": "旅游和地点", "Objects": "物件", "Flags": "旗帜", "Characters": "字符", "Characters (no spaces)": "字符(无空格)", "{0} characters": "{0} 个字符", "Error: Form submit field collision.": "错误: 表单提交字段冲突。", "Error: No form element found.": "错误: 没有表单控件。", "Color swatch": "颜色样本", "Color Picker": "选色器", "Invalid hex color code: {0}": "十六进制颜色代码无效: {0}", "Invalid input": "无效输入", "R": "R", "Red component": "红色部分", "G": "G", "Green component": "绿色部分", "B": "B", "Blue component": "白色部分", "#": "#", "Hex color code": "十六进制颜色代码", "Range 0 to 255": "范围0至255", "Turquoise": "青绿色", "Green": "绿色", "Blue": "蓝色", "Purple": "紫色", "Navy Blue": "海军蓝", "Dark Turquoise": "深蓝绿色", "Dark Green": "深绿色", "Medium Blue": "中蓝色", "Medium Purple": "中紫色", "Midnight Blue": "深蓝色", "Yellow": "黄色", "Orange": "橙色", "Red": "红色", "Light Gray": "浅灰色", "Gray": "灰色", "Dark Yellow": "暗黄色", "Dark Orange": "深橙色", "Dark Red": "深红色", "Medium Gray": "中灰色", "Dark Gray": "深灰色", "Light Green": "浅绿色", "Light Yellow": "浅黄色", "Light Red": "浅红色", "Light Purple": "浅紫色", "Light Blue": "浅蓝色", "Dark Purple": "深紫色", "Dark Blue": "深蓝色", "Black": "黑色", "White": "白色", "Switch to or from fullscreen mode": "切换全屏模式", "Open help dialog": "打开帮助对话框", "history": "历史", "styles": "样式", "formatting": "格式化", "alignment": "对齐", "indentation": "缩进", "Font": "字体", "Size": "字号", "More...": "更多...", "Select...": "选择...", "Preferences": "首选项", "Yes": "是", "No": "否", "Keyboard Navigation": "键盘指引", "Version": "版本", "Code view": "代码视图", "Open popup menu for split buttons": "打开弹出式菜单,用于拆分按钮", "List Properties": "列表属性", "List properties...": "标题字体属性", "Start list at number": "以数字开始列表", "Line height": "行高", "Dropped file type is not supported": "此文件类型不支持拖放", "Loading...": "加载中...", "ImageProxy HTTP error: Rejected request": "图片代理请求错误:请求被拒绝", "ImageProxy HTTP error: Could not find Image Proxy": "图片代理请求错误:无法找到图片代理", "ImageProxy HTTP error: Incorrect Image Proxy URL": "图片代理请求错误:图片代理地址错误", "ImageProxy HTTP error: Unknown ImageProxy error": "图片代理请求错误:未知的图片代理错误" }); ================================================ FILE: vue_acimage_web/public/tinymce/license.txt ================================================ MIT License Copyright (c) 2022 Ephox Corporation DBA Tiny Technologies, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: vue_acimage_web/public/tinymce/plugins/emoticons/js/emojiimages.js ================================================ window.tinymce.Resource.add("tinymce.plugins.emoticons",{100:{keywords:["score","perfect","numbers","century","exam","quiz","test","pass","hundred"],char:'💯',fitzpatrick_scale:false,category:"symbols"},1234:{keywords:["numbers","blue-square"],char:'🔢',fitzpatrick_scale:false,category:"symbols"},grinning:{keywords:["face","smile","happy","joy",":D","grin"],char:'😀',fitzpatrick_scale:false,category:"people"},grimacing:{keywords:["face","grimace","teeth"],char:'😬',fitzpatrick_scale:false,category:"people"},grin:{keywords:["face","happy","smile","joy","kawaii"],char:'😁',fitzpatrick_scale:false,category:"people"},joy:{keywords:["face","cry","tears","weep","happy","happytears","haha"],char:'😂',fitzpatrick_scale:false,category:"people"},rofl:{keywords:["face","rolling","floor","laughing","lol","haha"],char:'🤣',fitzpatrick_scale:false,category:"people"},partying:{keywords:["face","celebration","woohoo"],char:'🥳',fitzpatrick_scale:false,category:"people"},smiley:{keywords:["face","happy","joy","haha",":D",":)","smile","funny"],char:'😃',fitzpatrick_scale:false,category:"people"},smile:{keywords:["face","happy","joy","funny","haha","laugh","like",":D",":)"],char:'😄',fitzpatrick_scale:false,category:"people"},sweat_smile:{keywords:["face","hot","happy","laugh","sweat","smile","relief"],char:'😅',fitzpatrick_scale:false,category:"people"},laughing:{keywords:["happy","joy","lol","satisfied","haha","face","glad","XD","laugh"],char:'😆',fitzpatrick_scale:false,category:"people"},innocent:{keywords:["face","angel","heaven","halo"],char:'😇',fitzpatrick_scale:false,category:"people"},wink:{keywords:["face","happy","mischievous","secret",";)","smile","eye"],char:'😉',fitzpatrick_scale:false,category:"people"},blush:{keywords:["face","smile","happy","flushed","crush","embarrassed","shy","joy"],char:'😊',fitzpatrick_scale:false,category:"people"},slightly_smiling_face:{keywords:["face","smile"],char:'🙂',fitzpatrick_scale:false,category:"people"},upside_down_face:{keywords:["face","flipped","silly","smile"],char:'🙃',fitzpatrick_scale:false,category:"people"},relaxed:{keywords:["face","blush","massage","happiness"],char:'☺️',fitzpatrick_scale:false,category:"people"},yum:{keywords:["happy","joy","tongue","smile","face","silly","yummy","nom","delicious","savouring"],char:'😋',fitzpatrick_scale:false,category:"people"},relieved:{keywords:["face","relaxed","phew","massage","happiness"],char:'😌',fitzpatrick_scale:false,category:"people"},heart_eyes:{keywords:["face","love","like","affection","valentines","infatuation","crush","heart"],char:'😍',fitzpatrick_scale:false,category:"people"},smiling_face_with_three_hearts:{keywords:["face","love","like","affection","valentines","infatuation","crush","hearts","adore"],char:'🥰',fitzpatrick_scale:false,category:"people"},kissing_heart:{keywords:["face","love","like","affection","valentines","infatuation","kiss"],char:'😘',fitzpatrick_scale:false,category:"people"},kissing:{keywords:["love","like","face","3","valentines","infatuation","kiss"],char:'😗',fitzpatrick_scale:false,category:"people"},kissing_smiling_eyes:{keywords:["face","affection","valentines","infatuation","kiss"],char:'😙',fitzpatrick_scale:false,category:"people"},kissing_closed_eyes:{keywords:["face","love","like","affection","valentines","infatuation","kiss"],char:'😚',fitzpatrick_scale:false,category:"people"},stuck_out_tongue_winking_eye:{keywords:["face","prank","childish","playful","mischievous","smile","wink","tongue"],char:'😜',fitzpatrick_scale:false,category:"people"},zany:{keywords:["face","goofy","crazy"],char:'🤪',fitzpatrick_scale:false,category:"people"},raised_eyebrow:{keywords:["face","distrust","scepticism","disapproval","disbelief","surprise"],char:'🤨',fitzpatrick_scale:false,category:"people"},monocle:{keywords:["face","stuffy","wealthy"],char:'🧐',fitzpatrick_scale:false,category:"people"},stuck_out_tongue_closed_eyes:{keywords:["face","prank","playful","mischievous","smile","tongue"],char:'😝',fitzpatrick_scale:false,category:"people"},stuck_out_tongue:{keywords:["face","prank","childish","playful","mischievous","smile","tongue"],char:'😛',fitzpatrick_scale:false,category:"people"},money_mouth_face:{keywords:["face","rich","dollar","money"],char:'🤑',fitzpatrick_scale:false,category:"people"},nerd_face:{keywords:["face","nerdy","geek","dork"],char:'🤓',fitzpatrick_scale:false,category:"people"},sunglasses:{keywords:["face","cool","smile","summer","beach","sunglass"],char:'😎',fitzpatrick_scale:false,category:"people"},star_struck:{keywords:["face","smile","starry","eyes","grinning"],char:'🤩',fitzpatrick_scale:false,category:"people"},clown_face:{keywords:["face"],char:'🤡',fitzpatrick_scale:false,category:"people"},cowboy_hat_face:{keywords:["face","cowgirl","hat"],char:'🤠',fitzpatrick_scale:false,category:"people"},hugs:{keywords:["face","smile","hug"],char:'🤗',fitzpatrick_scale:false,category:"people"},smirk:{keywords:["face","smile","mean","prank","smug","sarcasm"],char:'😏',fitzpatrick_scale:false,category:"people"},no_mouth:{keywords:["face","hellokitty"],char:'😶',fitzpatrick_scale:false,category:"people"},neutral_face:{keywords:["indifference","meh",":|","neutral"],char:'😐',fitzpatrick_scale:false,category:"people"},expressionless:{keywords:["face","indifferent","-_-","meh","deadpan"],char:'😑',fitzpatrick_scale:false,category:"people"},unamused:{keywords:["indifference","bored","straight face","serious","sarcasm","unimpressed","skeptical","dubious","side_eye"],char:'😒',fitzpatrick_scale:false,category:"people"},roll_eyes:{keywords:["face","eyeroll","frustrated"],char:'🙄',fitzpatrick_scale:false,category:"people"},thinking:{keywords:["face","hmmm","think","consider"],char:'🤔',fitzpatrick_scale:false,category:"people"},lying_face:{keywords:["face","lie","pinocchio"],char:'🤥',fitzpatrick_scale:false,category:"people"},hand_over_mouth:{keywords:["face","whoops","shock","surprise"],char:'🤭',fitzpatrick_scale:false,category:"people"},shushing:{keywords:["face","quiet","shhh"],char:'🤫',fitzpatrick_scale:false,category:"people"},symbols_over_mouth:{keywords:["face","swearing","cursing","cussing","profanity","expletive"],char:'🤬',fitzpatrick_scale:false,category:"people"},exploding_head:{keywords:["face","shocked","mind","blown"],char:'🤯',fitzpatrick_scale:false,category:"people"},flushed:{keywords:["face","blush","shy","flattered"],char:'😳',fitzpatrick_scale:false,category:"people"},disappointed:{keywords:["face","sad","upset","depressed",":("],char:'😞',fitzpatrick_scale:false,category:"people"},worried:{keywords:["face","concern","nervous",":("],char:'😟',fitzpatrick_scale:false,category:"people"},angry:{keywords:["mad","face","annoyed","frustrated"],char:'😠',fitzpatrick_scale:false,category:"people"},rage:{keywords:["angry","mad","hate","despise"],char:'😡',fitzpatrick_scale:false,category:"people"},pensive:{keywords:["face","sad","depressed","upset"],char:'😔',fitzpatrick_scale:false,category:"people"},confused:{keywords:["face","indifference","huh","weird","hmmm",":/"],char:'😕',fitzpatrick_scale:false,category:"people"},slightly_frowning_face:{keywords:["face","frowning","disappointed","sad","upset"],char:'🙁',fitzpatrick_scale:false,category:"people"},frowning_face:{keywords:["face","sad","upset","frown"],char:'☹',fitzpatrick_scale:false,category:"people"},persevere:{keywords:["face","sick","no","upset","oops"],char:'😣',fitzpatrick_scale:false,category:"people"},confounded:{keywords:["face","confused","sick","unwell","oops",":S"],char:'😖',fitzpatrick_scale:false,category:"people"},tired_face:{keywords:["sick","whine","upset","frustrated"],char:'😫',fitzpatrick_scale:false,category:"people"},weary:{keywords:["face","tired","sleepy","sad","frustrated","upset"],char:'😩',fitzpatrick_scale:false,category:"people"},pleading:{keywords:["face","begging","mercy"],char:'🥺',fitzpatrick_scale:false,category:"people"},triumph:{keywords:["face","gas","phew","proud","pride"],char:'😤',fitzpatrick_scale:false,category:"people"},open_mouth:{keywords:["face","surprise","impressed","wow","whoa",":O"],char:'😮',fitzpatrick_scale:false,category:"people"},scream:{keywords:["face","munch","scared","omg"],char:'😱',fitzpatrick_scale:false,category:"people"},fearful:{keywords:["face","scared","terrified","nervous","oops","huh"],char:'😨',fitzpatrick_scale:false,category:"people"},cold_sweat:{keywords:["face","nervous","sweat"],char:'😰',fitzpatrick_scale:false,category:"people"},hushed:{keywords:["face","woo","shh"],char:'😯',fitzpatrick_scale:false,category:"people"},frowning:{keywords:["face","aw","what"],char:'😦',fitzpatrick_scale:false,category:"people"},anguished:{keywords:["face","stunned","nervous"],char:'😧',fitzpatrick_scale:false,category:"people"},cry:{keywords:["face","tears","sad","depressed","upset",":'("],char:'😢',fitzpatrick_scale:false,category:"people"},disappointed_relieved:{keywords:["face","phew","sweat","nervous"],char:'😥',fitzpatrick_scale:false,category:"people"},drooling_face:{keywords:["face"],char:'🤤',fitzpatrick_scale:false,category:"people"},sleepy:{keywords:["face","tired","rest","nap"],char:'😪',fitzpatrick_scale:false,category:"people"},sweat:{keywords:["face","hot","sad","tired","exercise"],char:'😓',fitzpatrick_scale:false,category:"people"},hot:{keywords:["face","feverish","heat","red","sweating"],char:'🥵',fitzpatrick_scale:false,category:"people"},cold:{keywords:["face","blue","freezing","frozen","frostbite","icicles"],char:'🥶',fitzpatrick_scale:false,category:"people"},sob:{keywords:["face","cry","tears","sad","upset","depressed"],char:'😭',fitzpatrick_scale:false,category:"people"},dizzy_face:{keywords:["spent","unconscious","xox","dizzy"],char:'😵',fitzpatrick_scale:false,category:"people"},astonished:{keywords:["face","xox","surprised","poisoned"],char:'😲',fitzpatrick_scale:false,category:"people"},zipper_mouth_face:{keywords:["face","sealed","zipper","secret"],char:'🤐',fitzpatrick_scale:false,category:"people"},nauseated_face:{keywords:["face","vomit","gross","green","sick","throw up","ill"],char:'🤢',fitzpatrick_scale:false,category:"people"},sneezing_face:{keywords:["face","gesundheit","sneeze","sick","allergy"],char:'🤧',fitzpatrick_scale:false,category:"people"},vomiting:{keywords:["face","sick"],char:'🤮',fitzpatrick_scale:false,category:"people"},mask:{keywords:["face","sick","ill","disease"],char:'😷',fitzpatrick_scale:false,category:"people"},face_with_thermometer:{keywords:["sick","temperature","thermometer","cold","fever"],char:'🤒',fitzpatrick_scale:false,category:"people"},face_with_head_bandage:{keywords:["injured","clumsy","bandage","hurt"],char:'🤕',fitzpatrick_scale:false,category:"people"},woozy:{keywords:["face","dizzy","intoxicated","tipsy","wavy"],char:'🥴',fitzpatrick_scale:false,category:"people"},sleeping:{keywords:["face","tired","sleepy","night","zzz"],char:'😴',fitzpatrick_scale:false,category:"people"},zzz:{keywords:["sleepy","tired","dream"],char:'💤',fitzpatrick_scale:false,category:"people"},poop:{keywords:["hankey","shitface","fail","turd","shit"],char:'💩',fitzpatrick_scale:false,category:"people"},smiling_imp:{keywords:["devil","horns"],char:'😈',fitzpatrick_scale:false,category:"people"},imp:{keywords:["devil","angry","horns"],char:'👿',fitzpatrick_scale:false,category:"people"},japanese_ogre:{keywords:["monster","red","mask","halloween","scary","creepy","devil","demon","japanese","ogre"],char:'👹',fitzpatrick_scale:false,category:"people"},japanese_goblin:{keywords:["red","evil","mask","monster","scary","creepy","japanese","goblin"],char:'👺',fitzpatrick_scale:false,category:"people"},skull:{keywords:["dead","skeleton","creepy","death"],char:'💀',fitzpatrick_scale:false,category:"people"},ghost:{keywords:["halloween","spooky","scary"],char:'👻',fitzpatrick_scale:false,category:"people"},alien:{keywords:["UFO","paul","weird","outer_space"],char:'👽',fitzpatrick_scale:false,category:"people"},robot:{keywords:["computer","machine","bot"],char:'🤖',fitzpatrick_scale:false,category:"people"},smiley_cat:{keywords:["animal","cats","happy","smile"],char:'😺',fitzpatrick_scale:false,category:"people"},smile_cat:{keywords:["animal","cats","smile"],char:'😸',fitzpatrick_scale:false,category:"people"},joy_cat:{keywords:["animal","cats","haha","happy","tears"],char:'😹',fitzpatrick_scale:false,category:"people"},heart_eyes_cat:{keywords:["animal","love","like","affection","cats","valentines","heart"],char:'😻',fitzpatrick_scale:false,category:"people"},smirk_cat:{keywords:["animal","cats","smirk"],char:'😼',fitzpatrick_scale:false,category:"people"},kissing_cat:{keywords:["animal","cats","kiss"],char:'😽',fitzpatrick_scale:false,category:"people"},scream_cat:{keywords:["animal","cats","munch","scared","scream"],char:'🙀',fitzpatrick_scale:false,category:"people"},crying_cat_face:{keywords:["animal","tears","weep","sad","cats","upset","cry"],char:'😿',fitzpatrick_scale:false,category:"people"},pouting_cat:{keywords:["animal","cats"],char:'😾',fitzpatrick_scale:false,category:"people"},palms_up:{keywords:["hands","gesture","cupped","prayer"],char:'🤲',fitzpatrick_scale:true,category:"people"},raised_hands:{keywords:["gesture","hooray","yea","celebration","hands"],char:'🙌',fitzpatrick_scale:true,category:"people"},clap:{keywords:["hands","praise","applause","congrats","yay"],char:'👏',fitzpatrick_scale:true,category:"people"},wave:{keywords:["hands","gesture","goodbye","solong","farewell","hello","hi","palm"],char:'👋',fitzpatrick_scale:true,category:"people"},call_me_hand:{keywords:["hands","gesture"],char:'🤙',fitzpatrick_scale:true,category:"people"},"+1":{keywords:["thumbsup","yes","awesome","good","agree","accept","cool","hand","like"],char:'👍',fitzpatrick_scale:true,category:"people"},"-1":{keywords:["thumbsdown","no","dislike","hand"],char:'👎',fitzpatrick_scale:true,category:"people"},facepunch:{keywords:["angry","violence","fist","hit","attack","hand"],char:'👊',fitzpatrick_scale:true,category:"people"},fist:{keywords:["fingers","hand","grasp"],char:'✊',fitzpatrick_scale:true,category:"people"},fist_left:{keywords:["hand","fistbump"],char:'🤛',fitzpatrick_scale:true,category:"people"},fist_right:{keywords:["hand","fistbump"],char:'🤜',fitzpatrick_scale:true,category:"people"},v:{keywords:["fingers","ohyeah","hand","peace","victory","two"],char:'✌',fitzpatrick_scale:true,category:"people"},ok_hand:{keywords:["fingers","limbs","perfect","ok","okay"],char:'👌',fitzpatrick_scale:true,category:"people"},raised_hand:{keywords:["fingers","stop","highfive","palm","ban"],char:'✋',fitzpatrick_scale:true,category:"people"},raised_back_of_hand:{keywords:["fingers","raised","backhand"],char:'🤚',fitzpatrick_scale:true,category:"people"},open_hands:{keywords:["fingers","butterfly","hands","open"],char:'👐',fitzpatrick_scale:true,category:"people"},muscle:{keywords:["arm","flex","hand","summer","strong","biceps"],char:'💪',fitzpatrick_scale:true,category:"people"},pray:{keywords:["please","hope","wish","namaste","highfive"],char:'🙏',fitzpatrick_scale:true,category:"people"},foot:{keywords:["kick","stomp"],char:'🦶',fitzpatrick_scale:true,category:"people"},leg:{keywords:["kick","limb"],char:'🦵',fitzpatrick_scale:true,category:"people"},handshake:{keywords:["agreement","shake"],char:'🤝',fitzpatrick_scale:false,category:"people"},point_up:{keywords:["hand","fingers","direction","up"],char:'☝',fitzpatrick_scale:true,category:"people"},point_up_2:{keywords:["fingers","hand","direction","up"],char:'👆',fitzpatrick_scale:true,category:"people"},point_down:{keywords:["fingers","hand","direction","down"],char:'👇',fitzpatrick_scale:true,category:"people"},point_left:{keywords:["direction","fingers","hand","left"],char:'👈',fitzpatrick_scale:true,category:"people"},point_right:{keywords:["fingers","hand","direction","right"],char:'👉',fitzpatrick_scale:true,category:"people"},fu:{keywords:["hand","fingers","rude","middle","flipping"],char:'🖕',fitzpatrick_scale:true,category:"people"},raised_hand_with_fingers_splayed:{keywords:["hand","fingers","palm"],char:'🖐',fitzpatrick_scale:true,category:"people"},love_you:{keywords:["hand","fingers","gesture"],char:'🤟',fitzpatrick_scale:true,category:"people"},metal:{keywords:["hand","fingers","evil_eye","sign_of_horns","rock_on"],char:'🤘',fitzpatrick_scale:true,category:"people"},crossed_fingers:{keywords:["good","lucky"],char:'🤞',fitzpatrick_scale:true,category:"people"},vulcan_salute:{keywords:["hand","fingers","spock","star trek"],char:'🖖',fitzpatrick_scale:true,category:"people"},writing_hand:{keywords:["lower_left_ballpoint_pen","stationery","write","compose"],char:'✍',fitzpatrick_scale:true,category:"people"},selfie:{keywords:["camera","phone"],char:'🤳',fitzpatrick_scale:true,category:"people"},nail_care:{keywords:["beauty","manicure","finger","fashion","nail"],char:'💅',fitzpatrick_scale:true,category:"people"},lips:{keywords:["mouth","kiss"],char:'👄',fitzpatrick_scale:false,category:"people"},tooth:{keywords:["teeth","dentist"],char:'🦷',fitzpatrick_scale:false,category:"people"},tongue:{keywords:["mouth","playful"],char:'👅',fitzpatrick_scale:false,category:"people"},ear:{keywords:["face","hear","sound","listen"],char:'👂',fitzpatrick_scale:true,category:"people"},nose:{keywords:["smell","sniff"],char:'👃',fitzpatrick_scale:true,category:"people"},eye:{keywords:["face","look","see","watch","stare"],char:'👁',fitzpatrick_scale:false,category:"people"},eyes:{keywords:["look","watch","stalk","peek","see"],char:'👀',fitzpatrick_scale:false,category:"people"},brain:{keywords:["smart","intelligent"],char:'🧠',fitzpatrick_scale:false,category:"people"},bust_in_silhouette:{keywords:["user","person","human"],char:'👤',fitzpatrick_scale:false,category:"people"},busts_in_silhouette:{keywords:["user","person","human","group","team"],char:'👥',fitzpatrick_scale:false,category:"people"},speaking_head:{keywords:["user","person","human","sing","say","talk"],char:'🗣',fitzpatrick_scale:false,category:"people"},baby:{keywords:["child","boy","girl","toddler"],char:'👶',fitzpatrick_scale:true,category:"people"},child:{keywords:["gender-neutral","young"],char:'🧒',fitzpatrick_scale:true,category:"people"},boy:{keywords:["man","male","guy","teenager"],char:'👦',fitzpatrick_scale:true,category:"people"},girl:{keywords:["female","woman","teenager"],char:'👧',fitzpatrick_scale:true,category:"people"},adult:{keywords:["gender-neutral","person"],char:'🧑',fitzpatrick_scale:true,category:"people"},man:{keywords:["mustache","father","dad","guy","classy","sir","moustache"],char:'👨',fitzpatrick_scale:true,category:"people"},woman:{keywords:["female","girls","lady"],char:'👩',fitzpatrick_scale:true,category:"people"},blonde_woman:{keywords:["woman","female","girl","blonde","person"],char:'👱‍♀️',fitzpatrick_scale:true,category:"people"},blonde_man:{keywords:["man","male","boy","blonde","guy","person"],char:'👱',fitzpatrick_scale:true,category:"people"},bearded_person:{keywords:["person","bewhiskered"],char:'🧔',fitzpatrick_scale:true,category:"people"},older_adult:{keywords:["human","elder","senior","gender-neutral"],char:'🧓',fitzpatrick_scale:true,category:"people"},older_man:{keywords:["human","male","men","old","elder","senior"],char:'👴',fitzpatrick_scale:true,category:"people"},older_woman:{keywords:["human","female","women","lady","old","elder","senior"],char:'👵',fitzpatrick_scale:true,category:"people"},man_with_gua_pi_mao:{keywords:["male","boy","chinese"],char:'👲',fitzpatrick_scale:true,category:"people"},woman_with_headscarf:{keywords:["female","hijab","mantilla","tichel"],char:'🧕',fitzpatrick_scale:true,category:"people"},woman_with_turban:{keywords:["female","indian","hinduism","arabs","woman"],char:'👳‍♀️',fitzpatrick_scale:true,category:"people"},man_with_turban:{keywords:["male","indian","hinduism","arabs"],char:'👳',fitzpatrick_scale:true,category:"people"},policewoman:{keywords:["woman","police","law","legal","enforcement","arrest","911","female"],char:'👮‍♀️',fitzpatrick_scale:true,category:"people"},policeman:{keywords:["man","police","law","legal","enforcement","arrest","911"],char:'👮',fitzpatrick_scale:true,category:"people"},construction_worker_woman:{keywords:["female","human","wip","build","construction","worker","labor","woman"],char:'👷‍♀️',fitzpatrick_scale:true,category:"people"},construction_worker_man:{keywords:["male","human","wip","guy","build","construction","worker","labor"],char:'👷',fitzpatrick_scale:true,category:"people"},guardswoman:{keywords:["uk","gb","british","female","royal","woman"],char:'💂‍♀️',fitzpatrick_scale:true,category:"people"},guardsman:{keywords:["uk","gb","british","male","guy","royal"],char:'💂',fitzpatrick_scale:true,category:"people"},female_detective:{keywords:["human","spy","detective","female","woman"],char:'🕵️‍♀️',fitzpatrick_scale:true,category:"people"},male_detective:{keywords:["human","spy","detective"],char:'🕵',fitzpatrick_scale:true,category:"people"},woman_health_worker:{keywords:["doctor","nurse","therapist","healthcare","woman","human"],char:'👩‍⚕️',fitzpatrick_scale:true,category:"people"},man_health_worker:{keywords:["doctor","nurse","therapist","healthcare","man","human"],char:'👨‍⚕️',fitzpatrick_scale:true,category:"people"},woman_farmer:{keywords:["rancher","gardener","woman","human"],char:'👩‍🌾',fitzpatrick_scale:true,category:"people"},man_farmer:{keywords:["rancher","gardener","man","human"],char:'👨‍🌾',fitzpatrick_scale:true,category:"people"},woman_cook:{keywords:["chef","woman","human"],char:'👩‍🍳',fitzpatrick_scale:true,category:"people"},man_cook:{keywords:["chef","man","human"],char:'👨‍🍳',fitzpatrick_scale:true,category:"people"},woman_student:{keywords:["graduate","woman","human"],char:'👩‍🎓',fitzpatrick_scale:true,category:"people"},man_student:{keywords:["graduate","man","human"],char:'👨‍🎓',fitzpatrick_scale:true,category:"people"},woman_singer:{keywords:["rockstar","entertainer","woman","human"],char:'👩‍🎤',fitzpatrick_scale:true,category:"people"},man_singer:{keywords:["rockstar","entertainer","man","human"],char:'👨‍🎤',fitzpatrick_scale:true,category:"people"},woman_teacher:{keywords:["instructor","professor","woman","human"],char:'👩‍🏫',fitzpatrick_scale:true,category:"people"},man_teacher:{keywords:["instructor","professor","man","human"],char:'👨‍🏫',fitzpatrick_scale:true,category:"people"},woman_factory_worker:{keywords:["assembly","industrial","woman","human"],char:'👩‍🏭',fitzpatrick_scale:true,category:"people"},man_factory_worker:{keywords:["assembly","industrial","man","human"],char:'👨‍🏭',fitzpatrick_scale:true,category:"people"},woman_technologist:{keywords:["coder","developer","engineer","programmer","software","woman","human","laptop","computer"],char:'👩‍💻',fitzpatrick_scale:true,category:"people"},man_technologist:{keywords:["coder","developer","engineer","programmer","software","man","human","laptop","computer"],char:'👨‍💻',fitzpatrick_scale:true,category:"people"},woman_office_worker:{keywords:["business","manager","woman","human"],char:'👩‍💼',fitzpatrick_scale:true,category:"people"},man_office_worker:{keywords:["business","manager","man","human"],char:'👨‍💼',fitzpatrick_scale:true,category:"people"},woman_mechanic:{keywords:["plumber","woman","human","wrench"],char:'👩‍🔧',fitzpatrick_scale:true,category:"people"},man_mechanic:{keywords:["plumber","man","human","wrench"],char:'👨‍🔧',fitzpatrick_scale:true,category:"people"},woman_scientist:{keywords:["biologist","chemist","engineer","physicist","woman","human"],char:'👩‍🔬',fitzpatrick_scale:true,category:"people"},man_scientist:{keywords:["biologist","chemist","engineer","physicist","man","human"],char:'👨‍🔬',fitzpatrick_scale:true,category:"people"},woman_artist:{keywords:["painter","woman","human"],char:'👩‍🎨',fitzpatrick_scale:true,category:"people"},man_artist:{keywords:["painter","man","human"],char:'👨‍🎨',fitzpatrick_scale:true,category:"people"},woman_firefighter:{keywords:["fireman","woman","human"],char:'👩‍🚒',fitzpatrick_scale:true,category:"people"},man_firefighter:{keywords:["fireman","man","human"],char:'👨‍🚒',fitzpatrick_scale:true,category:"people"},woman_pilot:{keywords:["aviator","plane","woman","human"],char:'👩‍✈️',fitzpatrick_scale:true,category:"people"},man_pilot:{keywords:["aviator","plane","man","human"],char:'👨‍✈️',fitzpatrick_scale:true,category:"people"},woman_astronaut:{keywords:["space","rocket","woman","human"],char:'👩‍🚀',fitzpatrick_scale:true,category:"people"},man_astronaut:{keywords:["space","rocket","man","human"],char:'👨‍🚀',fitzpatrick_scale:true,category:"people"},woman_judge:{keywords:["justice","court","woman","human"],char:'👩‍⚖️',fitzpatrick_scale:true,category:"people"},man_judge:{keywords:["justice","court","man","human"],char:'👨‍⚖️',fitzpatrick_scale:true,category:"people"},woman_superhero:{keywords:["woman","female","good","heroine","superpowers"],char:'🦸‍♀️',fitzpatrick_scale:true,category:"people"},man_superhero:{keywords:["man","male","good","hero","superpowers"],char:'🦸‍♂️',fitzpatrick_scale:true,category:"people"},woman_supervillain:{keywords:["woman","female","evil","bad","criminal","heroine","superpowers"],char:'🦹‍♀️',fitzpatrick_scale:true,category:"people"},man_supervillain:{keywords:["man","male","evil","bad","criminal","hero","superpowers"],char:'🦹‍♂️',fitzpatrick_scale:true,category:"people"},mrs_claus:{keywords:["woman","female","xmas","mother christmas"],char:'🤶',fitzpatrick_scale:true,category:"people"},santa:{keywords:["festival","man","male","xmas","father christmas"],char:'🎅',fitzpatrick_scale:true,category:"people"},sorceress:{keywords:["woman","female","mage","witch"],char:'🧙‍♀️',fitzpatrick_scale:true,category:"people"},wizard:{keywords:["man","male","mage","sorcerer"],char:'🧙‍♂️',fitzpatrick_scale:true,category:"people"},woman_elf:{keywords:["woman","female"],char:'🧝‍♀️',fitzpatrick_scale:true,category:"people"},man_elf:{keywords:["man","male"],char:'🧝‍♂️',fitzpatrick_scale:true,category:"people"},woman_vampire:{keywords:["woman","female"],char:'🧛‍♀️',fitzpatrick_scale:true,category:"people"},man_vampire:{keywords:["man","male","dracula"],char:'🧛‍♂️',fitzpatrick_scale:true,category:"people"},woman_zombie:{keywords:["woman","female","undead","walking dead"],char:'🧟‍♀️',fitzpatrick_scale:false,category:"people"},man_zombie:{keywords:["man","male","dracula","undead","walking dead"],char:'🧟‍♂️',fitzpatrick_scale:false,category:"people"},woman_genie:{keywords:["woman","female"],char:'🧞‍♀️',fitzpatrick_scale:false,category:"people"},man_genie:{keywords:["man","male"],char:'🧞‍♂️',fitzpatrick_scale:false,category:"people"},mermaid:{keywords:["woman","female","merwoman","ariel"],char:'🧜‍♀️',fitzpatrick_scale:true,category:"people"},merman:{keywords:["man","male","triton"],char:'🧜‍♂️',fitzpatrick_scale:true,category:"people"},woman_fairy:{keywords:["woman","female"],char:'🧚‍♀️',fitzpatrick_scale:true,category:"people"},man_fairy:{keywords:["man","male"],char:'🧚‍♂️',fitzpatrick_scale:true,category:"people"},angel:{keywords:["heaven","wings","halo"],char:'👼',fitzpatrick_scale:true,category:"people"},pregnant_woman:{keywords:["baby"],char:'🤰',fitzpatrick_scale:true,category:"people"},breastfeeding:{keywords:["nursing","baby"],char:'🤱',fitzpatrick_scale:true,category:"people"},princess:{keywords:["girl","woman","female","blond","crown","royal","queen"],char:'👸',fitzpatrick_scale:true,category:"people"},prince:{keywords:["boy","man","male","crown","royal","king"],char:'🤴',fitzpatrick_scale:true,category:"people"},bride_with_veil:{keywords:["couple","marriage","wedding","woman","bride"],char:'👰',fitzpatrick_scale:true,category:"people"},man_in_tuxedo:{keywords:["couple","marriage","wedding","groom"],char:'🤵',fitzpatrick_scale:true,category:"people"},running_woman:{keywords:["woman","walking","exercise","race","running","female"],char:'🏃‍♀️',fitzpatrick_scale:true,category:"people"},running_man:{keywords:["man","walking","exercise","race","running"],char:'🏃',fitzpatrick_scale:true,category:"people"},walking_woman:{keywords:["human","feet","steps","woman","female"],char:'🚶‍♀️',fitzpatrick_scale:true,category:"people"},walking_man:{keywords:["human","feet","steps"],char:'🚶',fitzpatrick_scale:true,category:"people"},dancer:{keywords:["female","girl","woman","fun"],char:'💃',fitzpatrick_scale:true,category:"people"},man_dancing:{keywords:["male","boy","fun","dancer"],char:'🕺',fitzpatrick_scale:true,category:"people"},dancing_women:{keywords:["female","bunny","women","girls"],char:'👯',fitzpatrick_scale:false,category:"people"},dancing_men:{keywords:["male","bunny","men","boys"],char:'👯‍♂️',fitzpatrick_scale:false,category:"people"},couple:{keywords:["pair","people","human","love","date","dating","like","affection","valentines","marriage"],char:'👫',fitzpatrick_scale:false,category:"people"},two_men_holding_hands:{keywords:["pair","couple","love","like","bromance","friendship","people","human"],char:'👬',fitzpatrick_scale:false,category:"people"},two_women_holding_hands:{keywords:["pair","friendship","couple","love","like","female","people","human"],char:'👭',fitzpatrick_scale:false,category:"people"},bowing_woman:{keywords:["woman","female","girl"],char:'🙇‍♀️',fitzpatrick_scale:true,category:"people"},bowing_man:{keywords:["man","male","boy"],char:'🙇',fitzpatrick_scale:true,category:"people"},man_facepalming:{keywords:["man","male","boy","disbelief"],char:'🤦‍♂️',fitzpatrick_scale:true,category:"people"},woman_facepalming:{keywords:["woman","female","girl","disbelief"],char:'🤦‍♀️',fitzpatrick_scale:true,category:"people"},woman_shrugging:{keywords:["woman","female","girl","confused","indifferent","doubt"],char:'🤷',fitzpatrick_scale:true,category:"people"},man_shrugging:{keywords:["man","male","boy","confused","indifferent","doubt"],char:'🤷‍♂️',fitzpatrick_scale:true,category:"people"},tipping_hand_woman:{keywords:["female","girl","woman","human","information"],char:'💁',fitzpatrick_scale:true,category:"people"},tipping_hand_man:{keywords:["male","boy","man","human","information"],char:'💁‍♂️',fitzpatrick_scale:true,category:"people"},no_good_woman:{keywords:["female","girl","woman","nope"],char:'🙅',fitzpatrick_scale:true,category:"people"},no_good_man:{keywords:["male","boy","man","nope"],char:'🙅‍♂️',fitzpatrick_scale:true,category:"people"},ok_woman:{keywords:["women","girl","female","pink","human","woman"],char:'🙆',fitzpatrick_scale:true,category:"people"},ok_man:{keywords:["men","boy","male","blue","human","man"],char:'🙆‍♂️',fitzpatrick_scale:true,category:"people"},raising_hand_woman:{keywords:["female","girl","woman"],char:'🙋',fitzpatrick_scale:true,category:"people"},raising_hand_man:{keywords:["male","boy","man"],char:'🙋‍♂️',fitzpatrick_scale:true,category:"people"},pouting_woman:{keywords:["female","girl","woman"],char:'🙎',fitzpatrick_scale:true,category:"people"},pouting_man:{keywords:["male","boy","man"],char:'🙎‍♂️',fitzpatrick_scale:true,category:"people"},frowning_woman:{keywords:["female","girl","woman","sad","depressed","discouraged","unhappy"],char:'🙍',fitzpatrick_scale:true,category:"people"},frowning_man:{keywords:["male","boy","man","sad","depressed","discouraged","unhappy"],char:'🙍‍♂️',fitzpatrick_scale:true,category:"people"},haircut_woman:{keywords:["female","girl","woman"],char:'💇',fitzpatrick_scale:true,category:"people"},haircut_man:{keywords:["male","boy","man"],char:'💇‍♂️',fitzpatrick_scale:true,category:"people"},massage_woman:{keywords:["female","girl","woman","head"],char:'💆',fitzpatrick_scale:true,category:"people"},massage_man:{keywords:["male","boy","man","head"],char:'💆‍♂️',fitzpatrick_scale:true,category:"people"},woman_in_steamy_room:{keywords:["female","woman","spa","steamroom","sauna"],char:'🧖‍♀️',fitzpatrick_scale:true,category:"people"},man_in_steamy_room:{keywords:["male","man","spa","steamroom","sauna"],char:'🧖‍♂️',fitzpatrick_scale:true,category:"people"},couple_with_heart_woman_man:{keywords:["pair","love","like","affection","human","dating","valentines","marriage"],char:'💑',fitzpatrick_scale:false,category:"people"},couple_with_heart_woman_woman:{keywords:["pair","love","like","affection","human","dating","valentines","marriage"],char:'👩‍❤️‍👩',fitzpatrick_scale:false,category:"people"},couple_with_heart_man_man:{keywords:["pair","love","like","affection","human","dating","valentines","marriage"],char:'👨‍❤️‍👨',fitzpatrick_scale:false,category:"people"},couplekiss_man_woman:{keywords:["pair","valentines","love","like","dating","marriage"],char:'💏',fitzpatrick_scale:false,category:"people"},couplekiss_woman_woman:{keywords:["pair","valentines","love","like","dating","marriage"],char:'👩‍❤️‍💋‍👩',fitzpatrick_scale:false,category:"people"},couplekiss_man_man:{keywords:["pair","valentines","love","like","dating","marriage"],char:'👨‍❤️‍💋‍👨',fitzpatrick_scale:false,category:"people"},family_man_woman_boy:{keywords:["home","parents","child","mom","dad","father","mother","people","human"],char:'👪',fitzpatrick_scale:false,category:"people"},family_man_woman_girl:{keywords:["home","parents","people","human","child"],char:'👨‍👩‍👧',fitzpatrick_scale:false,category:"people"},family_man_woman_girl_boy:{keywords:["home","parents","people","human","children"],char:'👨‍👩‍👧‍👦',fitzpatrick_scale:false,category:"people"},family_man_woman_boy_boy:{keywords:["home","parents","people","human","children"],char:'👨‍👩‍👦‍👦',fitzpatrick_scale:false,category:"people"},family_man_woman_girl_girl:{keywords:["home","parents","people","human","children"],char:'👨‍👩‍👧‍👧',fitzpatrick_scale:false,category:"people"},family_woman_woman_boy:{keywords:["home","parents","people","human","children"],char:'👩‍👩‍👦',fitzpatrick_scale:false,category:"people"},family_woman_woman_girl:{keywords:["home","parents","people","human","children"],char:'👩‍👩‍👧',fitzpatrick_scale:false,category:"people"},family_woman_woman_girl_boy:{keywords:["home","parents","people","human","children"],char:'👩‍👩‍👧‍👦',fitzpatrick_scale:false,category:"people"},family_woman_woman_boy_boy:{keywords:["home","parents","people","human","children"],char:'👩‍👩‍👦‍👦',fitzpatrick_scale:false,category:"people"},family_woman_woman_girl_girl:{keywords:["home","parents","people","human","children"],char:'👩‍👩‍👧‍👧',fitzpatrick_scale:false,category:"people"},family_man_man_boy:{keywords:["home","parents","people","human","children"],char:'👨‍👨‍👦',fitzpatrick_scale:false,category:"people"},family_man_man_girl:{keywords:["home","parents","people","human","children"],char:'👨‍👨‍👧',fitzpatrick_scale:false,category:"people"},family_man_man_girl_boy:{keywords:["home","parents","people","human","children"],char:'👨‍👨‍👧‍👦',fitzpatrick_scale:false,category:"people"},family_man_man_boy_boy:{keywords:["home","parents","people","human","children"],char:'👨‍👨‍👦‍👦',fitzpatrick_scale:false,category:"people"},family_man_man_girl_girl:{keywords:["home","parents","people","human","children"],char:'👨‍👨‍👧‍👧',fitzpatrick_scale:false,category:"people"},family_woman_boy:{keywords:["home","parent","people","human","child"],char:'👩‍👦',fitzpatrick_scale:false,category:"people"},family_woman_girl:{keywords:["home","parent","people","human","child"],char:'👩‍👧',fitzpatrick_scale:false,category:"people"},family_woman_girl_boy:{keywords:["home","parent","people","human","children"],char:'👩‍👧‍👦',fitzpatrick_scale:false,category:"people"},family_woman_boy_boy:{keywords:["home","parent","people","human","children"],char:'👩‍👦‍👦',fitzpatrick_scale:false,category:"people"},family_woman_girl_girl:{keywords:["home","parent","people","human","children"],char:'👩‍👧‍👧',fitzpatrick_scale:false,category:"people"},family_man_boy:{keywords:["home","parent","people","human","child"],char:'👨‍👦',fitzpatrick_scale:false,category:"people"},family_man_girl:{keywords:["home","parent","people","human","child"],char:'👨‍👧',fitzpatrick_scale:false,category:"people"},family_man_girl_boy:{keywords:["home","parent","people","human","children"],char:'👨‍👧‍👦',fitzpatrick_scale:false,category:"people"},family_man_boy_boy:{keywords:["home","parent","people","human","children"],char:'👨‍👦‍👦',fitzpatrick_scale:false,category:"people"},family_man_girl_girl:{keywords:["home","parent","people","human","children"],char:'👨‍👧‍👧',fitzpatrick_scale:false,category:"people"},yarn:{keywords:["ball","crochet","knit"],char:'🧶',fitzpatrick_scale:false,category:"people"},thread:{keywords:["needle","sewing","spool","string"],char:'🧵',fitzpatrick_scale:false,category:"people"},coat:{keywords:["jacket"],char:'🧥',fitzpatrick_scale:false,category:"people"},labcoat:{keywords:["doctor","experiment","scientist","chemist"],char:'🥼',fitzpatrick_scale:false,category:"people"},womans_clothes:{keywords:["fashion","shopping_bags","female"],char:'👚',fitzpatrick_scale:false,category:"people"},tshirt:{keywords:["fashion","cloth","casual","shirt","tee"],char:'👕',fitzpatrick_scale:false,category:"people"},jeans:{keywords:["fashion","shopping"],char:'👖',fitzpatrick_scale:false,category:"people"},necktie:{keywords:["shirt","suitup","formal","fashion","cloth","business"],char:'👔',fitzpatrick_scale:false,category:"people"},dress:{keywords:["clothes","fashion","shopping"],char:'👗',fitzpatrick_scale:false,category:"people"},bikini:{keywords:["swimming","female","woman","girl","fashion","beach","summer"],char:'👙',fitzpatrick_scale:false,category:"people"},kimono:{keywords:["dress","fashion","women","female","japanese"],char:'👘',fitzpatrick_scale:false,category:"people"},lipstick:{keywords:["female","girl","fashion","woman"],char:'💄',fitzpatrick_scale:false,category:"people"},kiss:{keywords:["face","lips","love","like","affection","valentines"],char:'💋',fitzpatrick_scale:false,category:"people"},footprints:{keywords:["feet","tracking","walking","beach"],char:'👣',fitzpatrick_scale:false,category:"people"},flat_shoe:{keywords:["ballet","slip-on","slipper"],char:'🥿',fitzpatrick_scale:false,category:"people"},high_heel:{keywords:["fashion","shoes","female","pumps","stiletto"],char:'👠',fitzpatrick_scale:false,category:"people"},sandal:{keywords:["shoes","fashion","flip flops"],char:'👡',fitzpatrick_scale:false,category:"people"},boot:{keywords:["shoes","fashion"],char:'👢',fitzpatrick_scale:false,category:"people"},mans_shoe:{keywords:["fashion","male"],char:'👞',fitzpatrick_scale:false,category:"people"},athletic_shoe:{keywords:["shoes","sports","sneakers"],char:'👟',fitzpatrick_scale:false,category:"people"},hiking_boot:{keywords:["backpacking","camping","hiking"],char:'🥾',fitzpatrick_scale:false,category:"people"},socks:{keywords:["stockings","clothes"],char:'🧦',fitzpatrick_scale:false,category:"people"},gloves:{keywords:["hands","winter","clothes"],char:'🧤',fitzpatrick_scale:false,category:"people"},scarf:{keywords:["neck","winter","clothes"],char:'🧣',fitzpatrick_scale:false,category:"people"},womans_hat:{keywords:["fashion","accessories","female","lady","spring"],char:'👒',fitzpatrick_scale:false,category:"people"},tophat:{keywords:["magic","gentleman","classy","circus"],char:'🎩',fitzpatrick_scale:false,category:"people"},billed_hat:{keywords:["cap","baseball"],char:'🧢',fitzpatrick_scale:false,category:"people"},rescue_worker_helmet:{keywords:["construction","build"],char:'⛑',fitzpatrick_scale:false,category:"people"},mortar_board:{keywords:["school","college","degree","university","graduation","cap","hat","legal","learn","education"],char:'🎓',fitzpatrick_scale:false,category:"people"},crown:{keywords:["king","kod","leader","royalty","lord"],char:'👑',fitzpatrick_scale:false,category:"people"},school_satchel:{keywords:["student","education","bag","backpack"],char:'🎒',fitzpatrick_scale:false,category:"people"},luggage:{keywords:["packing","travel"],char:'🧳',fitzpatrick_scale:false,category:"people"},pouch:{keywords:["bag","accessories","shopping"],char:'👝',fitzpatrick_scale:false,category:"people"},purse:{keywords:["fashion","accessories","money","sales","shopping"],char:'👛',fitzpatrick_scale:false,category:"people"},handbag:{keywords:["fashion","accessory","accessories","shopping"],char:'👜',fitzpatrick_scale:false,category:"people"},briefcase:{keywords:["business","documents","work","law","legal","job","career"],char:'💼',fitzpatrick_scale:false,category:"people"},eyeglasses:{keywords:["fashion","accessories","eyesight","nerdy","dork","geek"],char:'👓',fitzpatrick_scale:false,category:"people"},dark_sunglasses:{keywords:["face","cool","accessories"],char:'🕶',fitzpatrick_scale:false,category:"people"},goggles:{keywords:["eyes","protection","safety"],char:'🥽',fitzpatrick_scale:false,category:"people"},ring:{keywords:["wedding","propose","marriage","valentines","diamond","fashion","jewelry","gem","engagement"],char:'💍',fitzpatrick_scale:false,category:"people"},closed_umbrella:{keywords:["weather","rain","drizzle"],char:'🌂',fitzpatrick_scale:false,category:"people"},dog:{keywords:["animal","friend","nature","woof","puppy","pet","faithful"],char:'🐶',fitzpatrick_scale:false,category:"animals_and_nature"},cat:{keywords:["animal","meow","nature","pet","kitten"],char:'🐱',fitzpatrick_scale:false,category:"animals_and_nature"},mouse:{keywords:["animal","nature","cheese_wedge","rodent"],char:'🐭',fitzpatrick_scale:false,category:"animals_and_nature"},hamster:{keywords:["animal","nature"],char:'🐹',fitzpatrick_scale:false,category:"animals_and_nature"},rabbit:{keywords:["animal","nature","pet","spring","magic","bunny"],char:'🐰',fitzpatrick_scale:false,category:"animals_and_nature"},fox_face:{keywords:["animal","nature","face"],char:'🦊',fitzpatrick_scale:false,category:"animals_and_nature"},bear:{keywords:["animal","nature","wild"],char:'🐻',fitzpatrick_scale:false,category:"animals_and_nature"},panda_face:{keywords:["animal","nature","panda"],char:'🐼',fitzpatrick_scale:false,category:"animals_and_nature"},koala:{keywords:["animal","nature"],char:'🐨',fitzpatrick_scale:false,category:"animals_and_nature"},tiger:{keywords:["animal","cat","danger","wild","nature","roar"],char:'🐯',fitzpatrick_scale:false,category:"animals_and_nature"},lion:{keywords:["animal","nature"],char:'🦁',fitzpatrick_scale:false,category:"animals_and_nature"},cow:{keywords:["beef","ox","animal","nature","moo","milk"],char:'🐮',fitzpatrick_scale:false,category:"animals_and_nature"},pig:{keywords:["animal","oink","nature"],char:'🐷',fitzpatrick_scale:false,category:"animals_and_nature"},pig_nose:{keywords:["animal","oink"],char:'🐽',fitzpatrick_scale:false,category:"animals_and_nature"},frog:{keywords:["animal","nature","croak","toad"],char:'🐸',fitzpatrick_scale:false,category:"animals_and_nature"},squid:{keywords:["animal","nature","ocean","sea"],char:'🦑',fitzpatrick_scale:false,category:"animals_and_nature"},octopus:{keywords:["animal","creature","ocean","sea","nature","beach"],char:'🐙',fitzpatrick_scale:false,category:"animals_and_nature"},shrimp:{keywords:["animal","ocean","nature","seafood"],char:'🦐',fitzpatrick_scale:false,category:"animals_and_nature"},monkey_face:{keywords:["animal","nature","circus"],char:'🐵',fitzpatrick_scale:false,category:"animals_and_nature"},gorilla:{keywords:["animal","nature","circus"],char:'🦍',fitzpatrick_scale:false,category:"animals_and_nature"},see_no_evil:{keywords:["monkey","animal","nature","haha"],char:'🙈',fitzpatrick_scale:false,category:"animals_and_nature"},hear_no_evil:{keywords:["animal","monkey","nature"],char:'🙉',fitzpatrick_scale:false,category:"animals_and_nature"},speak_no_evil:{keywords:["monkey","animal","nature","omg"],char:'🙊',fitzpatrick_scale:false,category:"animals_and_nature"},monkey:{keywords:["animal","nature","banana","circus"],char:'🐒',fitzpatrick_scale:false,category:"animals_and_nature"},chicken:{keywords:["animal","cluck","nature","bird"],char:'🐔',fitzpatrick_scale:false,category:"animals_and_nature"},penguin:{keywords:["animal","nature"],char:'🐧',fitzpatrick_scale:false,category:"animals_and_nature"},bird:{keywords:["animal","nature","fly","tweet","spring"],char:'🐦',fitzpatrick_scale:false,category:"animals_and_nature"},baby_chick:{keywords:["animal","chicken","bird"],char:'🐤',fitzpatrick_scale:false,category:"animals_and_nature"},hatching_chick:{keywords:["animal","chicken","egg","born","baby","bird"],char:'🐣',fitzpatrick_scale:false,category:"animals_and_nature"},hatched_chick:{keywords:["animal","chicken","baby","bird"],char:'🐥',fitzpatrick_scale:false,category:"animals_and_nature"},duck:{keywords:["animal","nature","bird","mallard"],char:'🦆',fitzpatrick_scale:false,category:"animals_and_nature"},eagle:{keywords:["animal","nature","bird"],char:'🦅',fitzpatrick_scale:false,category:"animals_and_nature"},owl:{keywords:["animal","nature","bird","hoot"],char:'🦉',fitzpatrick_scale:false,category:"animals_and_nature"},bat:{keywords:["animal","nature","blind","vampire"],char:'🦇',fitzpatrick_scale:false,category:"animals_and_nature"},wolf:{keywords:["animal","nature","wild"],char:'🐺',fitzpatrick_scale:false,category:"animals_and_nature"},boar:{keywords:["animal","nature"],char:'🐗',fitzpatrick_scale:false,category:"animals_and_nature"},horse:{keywords:["animal","brown","nature"],char:'🐴',fitzpatrick_scale:false,category:"animals_and_nature"},unicorn:{keywords:["animal","nature","mystical"],char:'🦄',fitzpatrick_scale:false,category:"animals_and_nature"},honeybee:{keywords:["animal","insect","nature","bug","spring","honey"],char:'🐝',fitzpatrick_scale:false,category:"animals_and_nature"},bug:{keywords:["animal","insect","nature","worm"],char:'🐛',fitzpatrick_scale:false,category:"animals_and_nature"},butterfly:{keywords:["animal","insect","nature","caterpillar"],char:'🦋',fitzpatrick_scale:false,category:"animals_and_nature"},snail:{keywords:["slow","animal","shell"],char:'🐌',fitzpatrick_scale:false,category:"animals_and_nature"},beetle:{keywords:["animal","insect","nature","ladybug"],char:'🐞',fitzpatrick_scale:false,category:"animals_and_nature"},ant:{keywords:["animal","insect","nature","bug"],char:'🐜',fitzpatrick_scale:false,category:"animals_and_nature"},grasshopper:{keywords:["animal","cricket","chirp"],char:'🦗',fitzpatrick_scale:false,category:"animals_and_nature"},spider:{keywords:["animal","arachnid"],char:'🕷',fitzpatrick_scale:false,category:"animals_and_nature"},scorpion:{keywords:["animal","arachnid"],char:'🦂',fitzpatrick_scale:false,category:"animals_and_nature"},crab:{keywords:["animal","crustacean"],char:'🦀',fitzpatrick_scale:false,category:"animals_and_nature"},snake:{keywords:["animal","evil","nature","hiss","python"],char:'🐍',fitzpatrick_scale:false,category:"animals_and_nature"},lizard:{keywords:["animal","nature","reptile"],char:'🦎',fitzpatrick_scale:false,category:"animals_and_nature"},"t-rex":{keywords:["animal","nature","dinosaur","tyrannosaurus","extinct"],char:'🦖',fitzpatrick_scale:false,category:"animals_and_nature"},sauropod:{keywords:["animal","nature","dinosaur","brachiosaurus","brontosaurus","diplodocus","extinct"],char:'🦕',fitzpatrick_scale:false,category:"animals_and_nature"},turtle:{keywords:["animal","slow","nature","tortoise"],char:'🐢',fitzpatrick_scale:false,category:"animals_and_nature"},tropical_fish:{keywords:["animal","swim","ocean","beach","nemo"],char:'🐠',fitzpatrick_scale:false,category:"animals_and_nature"},fish:{keywords:["animal","food","nature"],char:'🐟',fitzpatrick_scale:false,category:"animals_and_nature"},blowfish:{keywords:["animal","nature","food","sea","ocean"],char:'🐡',fitzpatrick_scale:false,category:"animals_and_nature"},dolphin:{keywords:["animal","nature","fish","sea","ocean","flipper","fins","beach"],char:'🐬',fitzpatrick_scale:false,category:"animals_and_nature"},shark:{keywords:["animal","nature","fish","sea","ocean","jaws","fins","beach"],char:'🦈',fitzpatrick_scale:false,category:"animals_and_nature"},whale:{keywords:["animal","nature","sea","ocean"],char:'🐳',fitzpatrick_scale:false,category:"animals_and_nature"},whale2:{keywords:["animal","nature","sea","ocean"],char:'🐋',fitzpatrick_scale:false,category:"animals_and_nature"},crocodile:{keywords:["animal","nature","reptile","lizard","alligator"],char:'🐊',fitzpatrick_scale:false,category:"animals_and_nature"},leopard:{keywords:["animal","nature"],char:'🐆',fitzpatrick_scale:false,category:"animals_and_nature"},zebra:{keywords:["animal","nature","stripes","safari"],char:'🦓',fitzpatrick_scale:false,category:"animals_and_nature"},tiger2:{keywords:["animal","nature","roar"],char:'🐅',fitzpatrick_scale:false,category:"animals_and_nature"},water_buffalo:{keywords:["animal","nature","ox","cow"],char:'🐃',fitzpatrick_scale:false,category:"animals_and_nature"},ox:{keywords:["animal","cow","beef"],char:'🐂',fitzpatrick_scale:false,category:"animals_and_nature"},cow2:{keywords:["beef","ox","animal","nature","moo","milk"],char:'🐄',fitzpatrick_scale:false,category:"animals_and_nature"},deer:{keywords:["animal","nature","horns","venison"],char:'🦌',fitzpatrick_scale:false,category:"animals_and_nature"},dromedary_camel:{keywords:["animal","hot","desert","hump"],char:'🐪',fitzpatrick_scale:false,category:"animals_and_nature"},camel:{keywords:["animal","nature","hot","desert","hump"],char:'🐫',fitzpatrick_scale:false,category:"animals_and_nature"},giraffe:{keywords:["animal","nature","spots","safari"],char:'🦒',fitzpatrick_scale:false,category:"animals_and_nature"},elephant:{keywords:["animal","nature","nose","th","circus"],char:'🐘',fitzpatrick_scale:false,category:"animals_and_nature"},rhinoceros:{keywords:["animal","nature","horn"],char:'🦏',fitzpatrick_scale:false,category:"animals_and_nature"},goat:{keywords:["animal","nature"],char:'🐐',fitzpatrick_scale:false,category:"animals_and_nature"},ram:{keywords:["animal","sheep","nature"],char:'🐏',fitzpatrick_scale:false,category:"animals_and_nature"},sheep:{keywords:["animal","nature","wool","shipit"],char:'🐑',fitzpatrick_scale:false,category:"animals_and_nature"},racehorse:{keywords:["animal","gamble","luck"],char:'🐎',fitzpatrick_scale:false,category:"animals_and_nature"},pig2:{keywords:["animal","nature"],char:'🐖',fitzpatrick_scale:false,category:"animals_and_nature"},rat:{keywords:["animal","mouse","rodent"],char:'🐀',fitzpatrick_scale:false,category:"animals_and_nature"},mouse2:{keywords:["animal","nature","rodent"],char:'🐁',fitzpatrick_scale:false,category:"animals_and_nature"},rooster:{keywords:["animal","nature","chicken"],char:'🐓',fitzpatrick_scale:false,category:"animals_and_nature"},turkey:{keywords:["animal","bird"],char:'🦃',fitzpatrick_scale:false,category:"animals_and_nature"},dove:{keywords:["animal","bird"],char:'🕊',fitzpatrick_scale:false,category:"animals_and_nature"},dog2:{keywords:["animal","nature","friend","doge","pet","faithful"],char:'🐕',fitzpatrick_scale:false,category:"animals_and_nature"},poodle:{keywords:["dog","animal","101","nature","pet"],char:'🐩',fitzpatrick_scale:false,category:"animals_and_nature"},cat2:{keywords:["animal","meow","pet","cats"],char:'🐈',fitzpatrick_scale:false,category:"animals_and_nature"},rabbit2:{keywords:["animal","nature","pet","magic","spring"],char:'🐇',fitzpatrick_scale:false,category:"animals_and_nature"},chipmunk:{keywords:["animal","nature","rodent","squirrel"],char:'🐿',fitzpatrick_scale:false,category:"animals_and_nature"},hedgehog:{keywords:["animal","nature","spiny"],char:'🦔',fitzpatrick_scale:false,category:"animals_and_nature"},raccoon:{keywords:["animal","nature"],char:'🦝',fitzpatrick_scale:false,category:"animals_and_nature"},llama:{keywords:["animal","nature","alpaca"],char:'🦙',fitzpatrick_scale:false,category:"animals_and_nature"},hippopotamus:{keywords:["animal","nature"],char:'🦛',fitzpatrick_scale:false,category:"animals_and_nature"},kangaroo:{keywords:["animal","nature","australia","joey","hop","marsupial"],char:'🦘',fitzpatrick_scale:false,category:"animals_and_nature"},badger:{keywords:["animal","nature","honey"],char:'🦡',fitzpatrick_scale:false,category:"animals_and_nature"},swan:{keywords:["animal","nature","bird"],char:'🦢',fitzpatrick_scale:false,category:"animals_and_nature"},peacock:{keywords:["animal","nature","peahen","bird"],char:'🦚',fitzpatrick_scale:false,category:"animals_and_nature"},parrot:{keywords:["animal","nature","bird","pirate","talk"],char:'🦜',fitzpatrick_scale:false,category:"animals_and_nature"},lobster:{keywords:["animal","nature","bisque","claws","seafood"],char:'🦞',fitzpatrick_scale:false,category:"animals_and_nature"},mosquito:{keywords:["animal","nature","insect","malaria"],char:'🦟',fitzpatrick_scale:false,category:"animals_and_nature"},paw_prints:{keywords:["animal","tracking","footprints","dog","cat","pet","feet"],char:'🐾',fitzpatrick_scale:false,category:"animals_and_nature"},dragon:{keywords:["animal","myth","nature","chinese","green"],char:'🐉',fitzpatrick_scale:false,category:"animals_and_nature"},dragon_face:{keywords:["animal","myth","nature","chinese","green"],char:'🐲',fitzpatrick_scale:false,category:"animals_and_nature"},cactus:{keywords:["vegetable","plant","nature"],char:'🌵',fitzpatrick_scale:false,category:"animals_and_nature"},christmas_tree:{keywords:["festival","vacation","december","xmas","celebration"],char:'🎄',fitzpatrick_scale:false,category:"animals_and_nature"},evergreen_tree:{keywords:["plant","nature"],char:'🌲',fitzpatrick_scale:false,category:"animals_and_nature"},deciduous_tree:{keywords:["plant","nature"],char:'🌳',fitzpatrick_scale:false,category:"animals_and_nature"},palm_tree:{keywords:["plant","vegetable","nature","summer","beach","mojito","tropical"],char:'🌴',fitzpatrick_scale:false,category:"animals_and_nature"},seedling:{keywords:["plant","nature","grass","lawn","spring"],char:'🌱',fitzpatrick_scale:false,category:"animals_and_nature"},herb:{keywords:["vegetable","plant","medicine","weed","grass","lawn"],char:'🌿',fitzpatrick_scale:false,category:"animals_and_nature"},shamrock:{keywords:["vegetable","plant","nature","irish","clover"],char:'☘',fitzpatrick_scale:false,category:"animals_and_nature"},four_leaf_clover:{keywords:["vegetable","plant","nature","lucky","irish"],char:'🍀',fitzpatrick_scale:false,category:"animals_and_nature"},bamboo:{keywords:["plant","nature","vegetable","panda","pine_decoration"],char:'🎍',fitzpatrick_scale:false,category:"animals_and_nature"},tanabata_tree:{keywords:["plant","nature","branch","summer"],char:'🎋',fitzpatrick_scale:false,category:"animals_and_nature"},leaves:{keywords:["nature","plant","tree","vegetable","grass","lawn","spring"],char:'🍃',fitzpatrick_scale:false,category:"animals_and_nature"},fallen_leaf:{keywords:["nature","plant","vegetable","leaves"],char:'🍂',fitzpatrick_scale:false,category:"animals_and_nature"},maple_leaf:{keywords:["nature","plant","vegetable","ca","fall"],char:'🍁',fitzpatrick_scale:false,category:"animals_and_nature"},ear_of_rice:{keywords:["nature","plant"],char:'🌾',fitzpatrick_scale:false,category:"animals_and_nature"},hibiscus:{keywords:["plant","vegetable","flowers","beach"],char:'🌺',fitzpatrick_scale:false,category:"animals_and_nature"},sunflower:{keywords:["nature","plant","fall"],char:'🌻',fitzpatrick_scale:false,category:"animals_and_nature"},rose:{keywords:["flowers","valentines","love","spring"],char:'🌹',fitzpatrick_scale:false,category:"animals_and_nature"},wilted_flower:{keywords:["plant","nature","flower"],char:'🥀',fitzpatrick_scale:false,category:"animals_and_nature"},tulip:{keywords:["flowers","plant","nature","summer","spring"],char:'🌷',fitzpatrick_scale:false,category:"animals_and_nature"},blossom:{keywords:["nature","flowers","yellow"],char:'🌼',fitzpatrick_scale:false,category:"animals_and_nature"},cherry_blossom:{keywords:["nature","plant","spring","flower"],char:'🌸',fitzpatrick_scale:false,category:"animals_and_nature"},bouquet:{keywords:["flowers","nature","spring"],char:'💐',fitzpatrick_scale:false,category:"animals_and_nature"},mushroom:{keywords:["plant","vegetable"],char:'🍄',fitzpatrick_scale:false,category:"animals_and_nature"},chestnut:{keywords:["food","squirrel"],char:'🌰',fitzpatrick_scale:false,category:"animals_and_nature"},jack_o_lantern:{keywords:["halloween","light","pumpkin","creepy","fall"],char:'🎃',fitzpatrick_scale:false,category:"animals_and_nature"},shell:{keywords:["nature","sea","beach"],char:'🐚',fitzpatrick_scale:false,category:"animals_and_nature"},spider_web:{keywords:["animal","insect","arachnid","silk"],char:'🕸',fitzpatrick_scale:false,category:"animals_and_nature"},earth_americas:{keywords:["globe","world","USA","international"],char:'🌎',fitzpatrick_scale:false,category:"animals_and_nature"},earth_africa:{keywords:["globe","world","international"],char:'🌍',fitzpatrick_scale:false,category:"animals_and_nature"},earth_asia:{keywords:["globe","world","east","international"],char:'🌏',fitzpatrick_scale:false,category:"animals_and_nature"},full_moon:{keywords:["nature","yellow","twilight","planet","space","night","evening","sleep"],char:'🌕',fitzpatrick_scale:false,category:"animals_and_nature"},waning_gibbous_moon:{keywords:["nature","twilight","planet","space","night","evening","sleep","waxing_gibbous_moon"],char:'🌖',fitzpatrick_scale:false,category:"animals_and_nature"},last_quarter_moon:{keywords:["nature","twilight","planet","space","night","evening","sleep"],char:'🌗',fitzpatrick_scale:false,category:"animals_and_nature"},waning_crescent_moon:{keywords:["nature","twilight","planet","space","night","evening","sleep"],char:'🌘',fitzpatrick_scale:false,category:"animals_and_nature"},new_moon:{keywords:["nature","twilight","planet","space","night","evening","sleep"],char:'🌑',fitzpatrick_scale:false,category:"animals_and_nature"},waxing_crescent_moon:{keywords:["nature","twilight","planet","space","night","evening","sleep"],char:'🌒',fitzpatrick_scale:false,category:"animals_and_nature"},first_quarter_moon:{keywords:["nature","twilight","planet","space","night","evening","sleep"],char:'🌓',fitzpatrick_scale:false,category:"animals_and_nature"},waxing_gibbous_moon:{keywords:["nature","night","sky","gray","twilight","planet","space","evening","sleep"],char:'🌔',fitzpatrick_scale:false,category:"animals_and_nature"},new_moon_with_face:{keywords:["nature","twilight","planet","space","night","evening","sleep"],char:'🌚',fitzpatrick_scale:false,category:"animals_and_nature"},full_moon_with_face:{keywords:["nature","twilight","planet","space","night","evening","sleep"],char:'🌝',fitzpatrick_scale:false,category:"animals_and_nature"},first_quarter_moon_with_face:{keywords:["nature","twilight","planet","space","night","evening","sleep"],char:'🌛',fitzpatrick_scale:false,category:"animals_and_nature"},last_quarter_moon_with_face:{keywords:["nature","twilight","planet","space","night","evening","sleep"],char:'🌜',fitzpatrick_scale:false,category:"animals_and_nature"},sun_with_face:{keywords:["nature","morning","sky"],char:'🌞',fitzpatrick_scale:false,category:"animals_and_nature"},crescent_moon:{keywords:["night","sleep","sky","evening","magic"],char:'🌙',fitzpatrick_scale:false,category:"animals_and_nature"},star:{keywords:["night","yellow"],char:'⭐',fitzpatrick_scale:false,category:"animals_and_nature"},star2:{keywords:["night","sparkle","awesome","good","magic"],char:'🌟',fitzpatrick_scale:false,category:"animals_and_nature"},dizzy:{keywords:["star","sparkle","shoot","magic"],char:'💫',fitzpatrick_scale:false,category:"animals_and_nature"},sparkles:{keywords:["stars","shine","shiny","cool","awesome","good","magic"],char:'✨',fitzpatrick_scale:false,category:"animals_and_nature"},comet:{keywords:["space"],char:'☄',fitzpatrick_scale:false,category:"animals_and_nature"},sunny:{keywords:["weather","nature","brightness","summer","beach","spring"],char:'☀️',fitzpatrick_scale:false,category:"animals_and_nature"},sun_behind_small_cloud:{keywords:["weather"],char:'🌤',fitzpatrick_scale:false,category:"animals_and_nature"},partly_sunny:{keywords:["weather","nature","cloudy","morning","fall","spring"],char:'⛅',fitzpatrick_scale:false,category:"animals_and_nature"},sun_behind_large_cloud:{keywords:["weather"],char:'🌥',fitzpatrick_scale:false,category:"animals_and_nature"},sun_behind_rain_cloud:{keywords:["weather"],char:'🌦',fitzpatrick_scale:false,category:"animals_and_nature"},cloud:{keywords:["weather","sky"],char:'☁️',fitzpatrick_scale:false,category:"animals_and_nature"},cloud_with_rain:{keywords:["weather"],char:'🌧',fitzpatrick_scale:false,category:"animals_and_nature"},cloud_with_lightning_and_rain:{keywords:["weather","lightning"],char:'⛈',fitzpatrick_scale:false,category:"animals_and_nature"},cloud_with_lightning:{keywords:["weather","thunder"],char:'🌩',fitzpatrick_scale:false,category:"animals_and_nature"},zap:{keywords:["thunder","weather","lightning bolt","fast"],char:'⚡',fitzpatrick_scale:false,category:"animals_and_nature"},fire:{keywords:["hot","cook","flame"],char:'🔥',fitzpatrick_scale:false,category:"animals_and_nature"},boom:{keywords:["bomb","explode","explosion","collision","blown"],char:'💥',fitzpatrick_scale:false,category:"animals_and_nature"},snowflake:{keywords:["winter","season","cold","weather","christmas","xmas"],char:'❄️',fitzpatrick_scale:false,category:"animals_and_nature"},cloud_with_snow:{keywords:["weather"],char:'🌨',fitzpatrick_scale:false,category:"animals_and_nature"},snowman:{keywords:["winter","season","cold","weather","christmas","xmas","frozen","without_snow"],char:'⛄',fitzpatrick_scale:false,category:"animals_and_nature"},snowman_with_snow:{keywords:["winter","season","cold","weather","christmas","xmas","frozen"],char:'☃',fitzpatrick_scale:false,category:"animals_and_nature"},wind_face:{keywords:["gust","air"],char:'🌬',fitzpatrick_scale:false,category:"animals_and_nature"},dash:{keywords:["wind","air","fast","shoo","fart","smoke","puff"],char:'💨',fitzpatrick_scale:false,category:"animals_and_nature"},tornado:{keywords:["weather","cyclone","twister"],char:'🌪',fitzpatrick_scale:false,category:"animals_and_nature"},fog:{keywords:["weather"],char:'🌫',fitzpatrick_scale:false,category:"animals_and_nature"},open_umbrella:{keywords:["weather","spring"],char:'☂',fitzpatrick_scale:false,category:"animals_and_nature"},umbrella:{keywords:["rainy","weather","spring"],char:'☔',fitzpatrick_scale:false,category:"animals_and_nature"},droplet:{keywords:["water","drip","faucet","spring"],char:'💧',fitzpatrick_scale:false,category:"animals_and_nature"},sweat_drops:{keywords:["water","drip","oops"],char:'💦',fitzpatrick_scale:false,category:"animals_and_nature"},ocean:{keywords:["sea","water","wave","nature","tsunami","disaster"],char:'🌊',fitzpatrick_scale:false,category:"animals_and_nature"},green_apple:{keywords:["fruit","nature"],char:'🍏',fitzpatrick_scale:false,category:"food_and_drink"},apple:{keywords:["fruit","mac","school"],char:'🍎',fitzpatrick_scale:false,category:"food_and_drink"},pear:{keywords:["fruit","nature","food"],char:'🍐',fitzpatrick_scale:false,category:"food_and_drink"},tangerine:{keywords:["food","fruit","nature","orange"],char:'🍊',fitzpatrick_scale:false,category:"food_and_drink"},lemon:{keywords:["fruit","nature"],char:'🍋',fitzpatrick_scale:false,category:"food_and_drink"},banana:{keywords:["fruit","food","monkey"],char:'🍌',fitzpatrick_scale:false,category:"food_and_drink"},watermelon:{keywords:["fruit","food","picnic","summer"],char:'🍉',fitzpatrick_scale:false,category:"food_and_drink"},grapes:{keywords:["fruit","food","wine"],char:'🍇',fitzpatrick_scale:false,category:"food_and_drink"},strawberry:{keywords:["fruit","food","nature"],char:'🍓',fitzpatrick_scale:false,category:"food_and_drink"},melon:{keywords:["fruit","nature","food"],char:'🍈',fitzpatrick_scale:false,category:"food_and_drink"},cherries:{keywords:["food","fruit"],char:'🍒',fitzpatrick_scale:false,category:"food_and_drink"},peach:{keywords:["fruit","nature","food"],char:'🍑',fitzpatrick_scale:false,category:"food_and_drink"},pineapple:{keywords:["fruit","nature","food"],char:'🍍',fitzpatrick_scale:false,category:"food_and_drink"},coconut:{keywords:["fruit","nature","food","palm"],char:'🥥',fitzpatrick_scale:false,category:"food_and_drink"},kiwi_fruit:{keywords:["fruit","food"],char:'🥝',fitzpatrick_scale:false,category:"food_and_drink"},mango:{keywords:["fruit","food","tropical"],char:'🥭',fitzpatrick_scale:false,category:"food_and_drink"},avocado:{keywords:["fruit","food"],char:'🥑',fitzpatrick_scale:false,category:"food_and_drink"},broccoli:{keywords:["fruit","food","vegetable"],char:'🥦',fitzpatrick_scale:false,category:"food_and_drink"},tomato:{keywords:["fruit","vegetable","nature","food"],char:'🍅',fitzpatrick_scale:false,category:"food_and_drink"},eggplant:{keywords:["vegetable","nature","food","aubergine"],char:'🍆',fitzpatrick_scale:false,category:"food_and_drink"},cucumber:{keywords:["fruit","food","pickle"],char:'🥒',fitzpatrick_scale:false,category:"food_and_drink"},carrot:{keywords:["vegetable","food","orange"],char:'🥕',fitzpatrick_scale:false,category:"food_and_drink"},hot_pepper:{keywords:["food","spicy","chilli","chili"],char:'🌶',fitzpatrick_scale:false,category:"food_and_drink"},potato:{keywords:["food","tuber","vegatable","starch"],char:'🥔',fitzpatrick_scale:false,category:"food_and_drink"},corn:{keywords:["food","vegetable","plant"],char:'🌽',fitzpatrick_scale:false,category:"food_and_drink"},leafy_greens:{keywords:["food","vegetable","plant","bok choy","cabbage","kale","lettuce"],char:'🥬',fitzpatrick_scale:false,category:"food_and_drink"},sweet_potato:{keywords:["food","nature"],char:'🍠',fitzpatrick_scale:false,category:"food_and_drink"},peanuts:{keywords:["food","nut"],char:'🥜',fitzpatrick_scale:false,category:"food_and_drink"},honey_pot:{keywords:["bees","sweet","kitchen"],char:'🍯',fitzpatrick_scale:false,category:"food_and_drink"},croissant:{keywords:["food","bread","french"],char:'🥐',fitzpatrick_scale:false,category:"food_and_drink"},bread:{keywords:["food","wheat","breakfast","toast"],char:'🍞',fitzpatrick_scale:false,category:"food_and_drink"},baguette_bread:{keywords:["food","bread","french"],char:'🥖',fitzpatrick_scale:false,category:"food_and_drink"},bagel:{keywords:["food","bread","bakery","schmear"],char:'🥯',fitzpatrick_scale:false,category:"food_and_drink"},pretzel:{keywords:["food","bread","twisted"],char:'🥨',fitzpatrick_scale:false,category:"food_and_drink"},cheese:{keywords:["food","chadder"],char:'🧀',fitzpatrick_scale:false,category:"food_and_drink"},egg:{keywords:["food","chicken","breakfast"],char:'🥚',fitzpatrick_scale:false,category:"food_and_drink"},bacon:{keywords:["food","breakfast","pork","pig","meat"],char:'🥓',fitzpatrick_scale:false,category:"food_and_drink"},steak:{keywords:["food","cow","meat","cut","chop","lambchop","porkchop"],char:'🥩',fitzpatrick_scale:false,category:"food_and_drink"},pancakes:{keywords:["food","breakfast","flapjacks","hotcakes"],char:'🥞',fitzpatrick_scale:false,category:"food_and_drink"},poultry_leg:{keywords:["food","meat","drumstick","bird","chicken","turkey"],char:'🍗',fitzpatrick_scale:false,category:"food_and_drink"},meat_on_bone:{keywords:["good","food","drumstick"],char:'🍖',fitzpatrick_scale:false,category:"food_and_drink"},bone:{keywords:["skeleton"],char:'🦴',fitzpatrick_scale:false,category:"food_and_drink"},fried_shrimp:{keywords:["food","animal","appetizer","summer"],char:'🍤',fitzpatrick_scale:false,category:"food_and_drink"},fried_egg:{keywords:["food","breakfast","kitchen","egg"],char:'🍳',fitzpatrick_scale:false,category:"food_and_drink"},hamburger:{keywords:["meat","fast food","beef","cheeseburger","mcdonalds","burger king"],char:'🍔',fitzpatrick_scale:false,category:"food_and_drink"},fries:{keywords:["chips","snack","fast food"],char:'🍟',fitzpatrick_scale:false,category:"food_and_drink"},stuffed_flatbread:{keywords:["food","flatbread","stuffed","gyro"],char:'🥙',fitzpatrick_scale:false,category:"food_and_drink"},hotdog:{keywords:["food","frankfurter"],char:'🌭',fitzpatrick_scale:false,category:"food_and_drink"},pizza:{keywords:["food","party"],char:'🍕',fitzpatrick_scale:false,category:"food_and_drink"},sandwich:{keywords:["food","lunch","bread"],char:'🥪',fitzpatrick_scale:false,category:"food_and_drink"},canned_food:{keywords:["food","soup"],char:'🥫',fitzpatrick_scale:false,category:"food_and_drink"},spaghetti:{keywords:["food","italian","noodle"],char:'🍝',fitzpatrick_scale:false,category:"food_and_drink"},taco:{keywords:["food","mexican"],char:'🌮',fitzpatrick_scale:false,category:"food_and_drink"},burrito:{keywords:["food","mexican"],char:'🌯',fitzpatrick_scale:false,category:"food_and_drink"},green_salad:{keywords:["food","healthy","lettuce"],char:'🥗',fitzpatrick_scale:false,category:"food_and_drink"},shallow_pan_of_food:{keywords:["food","cooking","casserole","paella"],char:'🥘',fitzpatrick_scale:false,category:"food_and_drink"},ramen:{keywords:["food","japanese","noodle","chopsticks"],char:'🍜',fitzpatrick_scale:false,category:"food_and_drink"},stew:{keywords:["food","meat","soup"],char:'🍲',fitzpatrick_scale:false,category:"food_and_drink"},fish_cake:{keywords:["food","japan","sea","beach","narutomaki","pink","swirl","kamaboko","surimi","ramen"],char:'🍥',fitzpatrick_scale:false,category:"food_and_drink"},fortune_cookie:{keywords:["food","prophecy"],char:'🥠',fitzpatrick_scale:false,category:"food_and_drink"},sushi:{keywords:["food","fish","japanese","rice"],char:'🍣',fitzpatrick_scale:false,category:"food_and_drink"},bento:{keywords:["food","japanese","box"],char:'🍱',fitzpatrick_scale:false,category:"food_and_drink"},curry:{keywords:["food","spicy","hot","indian"],char:'🍛',fitzpatrick_scale:false,category:"food_and_drink"},rice_ball:{keywords:["food","japanese"],char:'🍙',fitzpatrick_scale:false,category:"food_and_drink"},rice:{keywords:["food","china","asian"],char:'🍚',fitzpatrick_scale:false,category:"food_and_drink"},rice_cracker:{keywords:["food","japanese"],char:'🍘',fitzpatrick_scale:false,category:"food_and_drink"},oden:{keywords:["food","japanese"],char:'🍢',fitzpatrick_scale:false,category:"food_and_drink"},dango:{keywords:["food","dessert","sweet","japanese","barbecue","meat"],char:'🍡',fitzpatrick_scale:false,category:"food_and_drink"},shaved_ice:{keywords:["hot","dessert","summer"],char:'🍧',fitzpatrick_scale:false,category:"food_and_drink"},ice_cream:{keywords:["food","hot","dessert"],char:'🍨',fitzpatrick_scale:false,category:"food_and_drink"},icecream:{keywords:["food","hot","dessert","summer"],char:'🍦',fitzpatrick_scale:false,category:"food_and_drink"},pie:{keywords:["food","dessert","pastry"],char:'🥧',fitzpatrick_scale:false,category:"food_and_drink"},cake:{keywords:["food","dessert"],char:'🍰',fitzpatrick_scale:false,category:"food_and_drink"},cupcake:{keywords:["food","dessert","bakery","sweet"],char:'🧁',fitzpatrick_scale:false,category:"food_and_drink"},moon_cake:{keywords:["food","autumn"],char:'🥮',fitzpatrick_scale:false,category:"food_and_drink"},birthday:{keywords:["food","dessert","cake"],char:'🎂',fitzpatrick_scale:false,category:"food_and_drink"},custard:{keywords:["dessert","food"],char:'🍮',fitzpatrick_scale:false,category:"food_and_drink"},candy:{keywords:["snack","dessert","sweet","lolly"],char:'🍬',fitzpatrick_scale:false,category:"food_and_drink"},lollipop:{keywords:["food","snack","candy","sweet"],char:'🍭',fitzpatrick_scale:false,category:"food_and_drink"},chocolate_bar:{keywords:["food","snack","dessert","sweet"],char:'🍫',fitzpatrick_scale:false,category:"food_and_drink"},popcorn:{keywords:["food","movie theater","films","snack"],char:'🍿',fitzpatrick_scale:false,category:"food_and_drink"},dumpling:{keywords:["food","empanada","pierogi","potsticker"],char:'🥟',fitzpatrick_scale:false,category:"food_and_drink"},doughnut:{keywords:["food","dessert","snack","sweet","donut"],char:'🍩',fitzpatrick_scale:false,category:"food_and_drink"},cookie:{keywords:["food","snack","oreo","chocolate","sweet","dessert"],char:'🍪',fitzpatrick_scale:false,category:"food_and_drink"},milk_glass:{keywords:["beverage","drink","cow"],char:'🥛',fitzpatrick_scale:false,category:"food_and_drink"},beer:{keywords:["relax","beverage","drink","drunk","party","pub","summer","alcohol","booze"],char:'🍺',fitzpatrick_scale:false,category:"food_and_drink"},beers:{keywords:["relax","beverage","drink","drunk","party","pub","summer","alcohol","booze"],char:'🍻',fitzpatrick_scale:false,category:"food_and_drink"},clinking_glasses:{keywords:["beverage","drink","party","alcohol","celebrate","cheers","wine","champagne","toast"],char:'🥂',fitzpatrick_scale:false,category:"food_and_drink"},wine_glass:{keywords:["drink","beverage","drunk","alcohol","booze"],char:'🍷',fitzpatrick_scale:false,category:"food_and_drink"},tumbler_glass:{keywords:["drink","beverage","drunk","alcohol","liquor","booze","bourbon","scotch","whisky","glass","shot"],char:'🥃',fitzpatrick_scale:false,category:"food_and_drink"},cocktail:{keywords:["drink","drunk","alcohol","beverage","booze","mojito"],char:'🍸',fitzpatrick_scale:false,category:"food_and_drink"},tropical_drink:{keywords:["beverage","cocktail","summer","beach","alcohol","booze","mojito"],char:'🍹',fitzpatrick_scale:false,category:"food_and_drink"},champagne:{keywords:["drink","wine","bottle","celebration"],char:'🍾',fitzpatrick_scale:false,category:"food_and_drink"},sake:{keywords:["wine","drink","drunk","beverage","japanese","alcohol","booze"],char:'🍶',fitzpatrick_scale:false,category:"food_and_drink"},tea:{keywords:["drink","bowl","breakfast","green","british"],char:'🍵',fitzpatrick_scale:false,category:"food_and_drink"},cup_with_straw:{keywords:["drink","soda"],char:'🥤',fitzpatrick_scale:false,category:"food_and_drink"},coffee:{keywords:["beverage","caffeine","latte","espresso"],char:'☕',fitzpatrick_scale:false,category:"food_and_drink"},baby_bottle:{keywords:["food","container","milk"],char:'🍼',fitzpatrick_scale:false,category:"food_and_drink"},salt:{keywords:["condiment","shaker"],char:'🧂',fitzpatrick_scale:false,category:"food_and_drink"},spoon:{keywords:["cutlery","kitchen","tableware"],char:'🥄',fitzpatrick_scale:false,category:"food_and_drink"},fork_and_knife:{keywords:["cutlery","kitchen"],char:'🍴',fitzpatrick_scale:false,category:"food_and_drink"},plate_with_cutlery:{keywords:["food","eat","meal","lunch","dinner","restaurant"],char:'🍽',fitzpatrick_scale:false,category:"food_and_drink"},bowl_with_spoon:{keywords:["food","breakfast","cereal","oatmeal","porridge"],char:'🥣',fitzpatrick_scale:false,category:"food_and_drink"},takeout_box:{keywords:["food","leftovers"],char:'🥡',fitzpatrick_scale:false,category:"food_and_drink"},chopsticks:{keywords:["food"],char:'🥢',fitzpatrick_scale:false,category:"food_and_drink"},soccer:{keywords:["sports","football"],char:'⚽',fitzpatrick_scale:false,category:"activity"},basketball:{keywords:["sports","balls","NBA"],char:'🏀',fitzpatrick_scale:false,category:"activity"},football:{keywords:["sports","balls","NFL"],char:'🏈',fitzpatrick_scale:false,category:"activity"},baseball:{keywords:["sports","balls"],char:'⚾',fitzpatrick_scale:false,category:"activity"},softball:{keywords:["sports","balls"],char:'🥎',fitzpatrick_scale:false,category:"activity"},tennis:{keywords:["sports","balls","green"],char:'🎾',fitzpatrick_scale:false,category:"activity"},volleyball:{keywords:["sports","balls"],char:'🏐',fitzpatrick_scale:false,category:"activity"},rugby_football:{keywords:["sports","team"],char:'🏉',fitzpatrick_scale:false,category:"activity"},flying_disc:{keywords:["sports","frisbee","ultimate"],char:'🥏',fitzpatrick_scale:false,category:"activity"},"8ball":{keywords:["pool","hobby","game","luck","magic"],char:'🎱',fitzpatrick_scale:false,category:"activity"},golf:{keywords:["sports","business","flag","hole","summer"],char:'⛳',fitzpatrick_scale:false,category:"activity"},golfing_woman:{keywords:["sports","business","woman","female"],char:'🏌️‍♀️',fitzpatrick_scale:false,category:"activity"},golfing_man:{keywords:["sports","business"],char:'🏌',fitzpatrick_scale:true,category:"activity"},ping_pong:{keywords:["sports","pingpong"],char:'🏓',fitzpatrick_scale:false,category:"activity"},badminton:{keywords:["sports"],char:'🏸',fitzpatrick_scale:false,category:"activity"},goal_net:{keywords:["sports"],char:'🥅',fitzpatrick_scale:false,category:"activity"},ice_hockey:{keywords:["sports"],char:'🏒',fitzpatrick_scale:false,category:"activity"},field_hockey:{keywords:["sports"],char:'🏑',fitzpatrick_scale:false,category:"activity"},lacrosse:{keywords:["sports","ball","stick"],char:'🥍',fitzpatrick_scale:false,category:"activity"},cricket:{keywords:["sports"],char:'🏏',fitzpatrick_scale:false,category:"activity"},ski:{keywords:["sports","winter","cold","snow"],char:'🎿',fitzpatrick_scale:false,category:"activity"},skier:{keywords:["sports","winter","snow"],char:'⛷',fitzpatrick_scale:false,category:"activity"},snowboarder:{keywords:["sports","winter"],char:'🏂',fitzpatrick_scale:true,category:"activity"},person_fencing:{keywords:["sports","fencing","sword"],char:'🤺',fitzpatrick_scale:false,category:"activity"},women_wrestling:{keywords:["sports","wrestlers"],char:'🤼‍♀️',fitzpatrick_scale:false,category:"activity"},men_wrestling:{keywords:["sports","wrestlers"],char:'🤼‍♂️',fitzpatrick_scale:false,category:"activity"},woman_cartwheeling:{keywords:["gymnastics"],char:'🤸‍♀️',fitzpatrick_scale:true,category:"activity"},man_cartwheeling:{keywords:["gymnastics"],char:'🤸‍♂️',fitzpatrick_scale:true,category:"activity"},woman_playing_handball:{keywords:["sports"],char:'🤾‍♀️',fitzpatrick_scale:true,category:"activity"},man_playing_handball:{keywords:["sports"],char:'🤾‍♂️',fitzpatrick_scale:true,category:"activity"},ice_skate:{keywords:["sports"],char:'⛸',fitzpatrick_scale:false,category:"activity"},curling_stone:{keywords:["sports"],char:'🥌',fitzpatrick_scale:false,category:"activity"},skateboard:{keywords:["board"],char:'🛹',fitzpatrick_scale:false,category:"activity"},sled:{keywords:["sleigh","luge","toboggan"],char:'🛷',fitzpatrick_scale:false,category:"activity"},bow_and_arrow:{keywords:["sports"],char:'🏹',fitzpatrick_scale:false,category:"activity"},fishing_pole_and_fish:{keywords:["food","hobby","summer"],char:'🎣',fitzpatrick_scale:false,category:"activity"},boxing_glove:{keywords:["sports","fighting"],char:'🥊',fitzpatrick_scale:false,category:"activity"},martial_arts_uniform:{keywords:["judo","karate","taekwondo"],char:'🥋',fitzpatrick_scale:false,category:"activity"},rowing_woman:{keywords:["sports","hobby","water","ship","woman","female"],char:'🚣‍♀️',fitzpatrick_scale:true,category:"activity"},rowing_man:{keywords:["sports","hobby","water","ship"],char:'🚣',fitzpatrick_scale:true,category:"activity"},climbing_woman:{keywords:["sports","hobby","woman","female","rock"],char:'🧗‍♀️',fitzpatrick_scale:true,category:"activity"},climbing_man:{keywords:["sports","hobby","man","male","rock"],char:'🧗‍♂️',fitzpatrick_scale:true,category:"activity"},swimming_woman:{keywords:["sports","exercise","human","athlete","water","summer","woman","female"],char:'🏊‍♀️',fitzpatrick_scale:true,category:"activity"},swimming_man:{keywords:["sports","exercise","human","athlete","water","summer"],char:'🏊',fitzpatrick_scale:true,category:"activity"},woman_playing_water_polo:{keywords:["sports","pool"],char:'🤽‍♀️',fitzpatrick_scale:true,category:"activity"},man_playing_water_polo:{keywords:["sports","pool"],char:'🤽‍♂️',fitzpatrick_scale:true,category:"activity"},woman_in_lotus_position:{keywords:["woman","female","meditation","yoga","serenity","zen","mindfulness"],char:'🧘‍♀️',fitzpatrick_scale:true,category:"activity"},man_in_lotus_position:{keywords:["man","male","meditation","yoga","serenity","zen","mindfulness"],char:'🧘‍♂️',fitzpatrick_scale:true,category:"activity"},surfing_woman:{keywords:["sports","ocean","sea","summer","beach","woman","female"],char:'🏄‍♀️',fitzpatrick_scale:true,category:"activity"},surfing_man:{keywords:["sports","ocean","sea","summer","beach"],char:'🏄',fitzpatrick_scale:true,category:"activity"},bath:{keywords:["clean","shower","bathroom"],char:'🛀',fitzpatrick_scale:true,category:"activity"},basketball_woman:{keywords:["sports","human","woman","female"],char:'⛹️‍♀️',fitzpatrick_scale:true,category:"activity"},basketball_man:{keywords:["sports","human"],char:'⛹',fitzpatrick_scale:true,category:"activity"},weight_lifting_woman:{keywords:["sports","training","exercise","woman","female"],char:'🏋️‍♀️',fitzpatrick_scale:true,category:"activity"},weight_lifting_man:{keywords:["sports","training","exercise"],char:'🏋',fitzpatrick_scale:true,category:"activity"},biking_woman:{keywords:["sports","bike","exercise","hipster","woman","female"],char:'🚴‍♀️',fitzpatrick_scale:true,category:"activity"},biking_man:{keywords:["sports","bike","exercise","hipster"],char:'🚴',fitzpatrick_scale:true,category:"activity"},mountain_biking_woman:{keywords:["transportation","sports","human","race","bike","woman","female"],char:'🚵‍♀️',fitzpatrick_scale:true,category:"activity"},mountain_biking_man:{keywords:["transportation","sports","human","race","bike"],char:'🚵',fitzpatrick_scale:true,category:"activity"},horse_racing:{keywords:["animal","betting","competition","gambling","luck"],char:'🏇',fitzpatrick_scale:true,category:"activity"},business_suit_levitating:{keywords:["suit","business","levitate","hover","jump"],char:'🕴',fitzpatrick_scale:true,category:"activity"},trophy:{keywords:["win","award","contest","place","ftw","ceremony"],char:'🏆',fitzpatrick_scale:false,category:"activity"},running_shirt_with_sash:{keywords:["play","pageant"],char:'🎽',fitzpatrick_scale:false,category:"activity"},medal_sports:{keywords:["award","winning"],char:'🏅',fitzpatrick_scale:false,category:"activity"},medal_military:{keywords:["award","winning","army"],char:'🎖',fitzpatrick_scale:false,category:"activity"},"1st_place_medal":{keywords:["award","winning","first"],char:'🥇',fitzpatrick_scale:false,category:"activity"},"2nd_place_medal":{keywords:["award","second"],char:'🥈',fitzpatrick_scale:false,category:"activity"},"3rd_place_medal":{keywords:["award","third"],char:'🥉',fitzpatrick_scale:false,category:"activity"},reminder_ribbon:{keywords:["sports","cause","support","awareness"],char:'🎗',fitzpatrick_scale:false,category:"activity"},rosette:{keywords:["flower","decoration","military"],char:'🏵',fitzpatrick_scale:false,category:"activity"},ticket:{keywords:["event","concert","pass"],char:'🎫',fitzpatrick_scale:false,category:"activity"},tickets:{keywords:["sports","concert","entrance"],char:'🎟',fitzpatrick_scale:false,category:"activity"},performing_arts:{keywords:["acting","theater","drama"],char:'🎭',fitzpatrick_scale:false,category:"activity"},art:{keywords:["design","paint","draw","colors"],char:'🎨',fitzpatrick_scale:false,category:"activity"},circus_tent:{keywords:["festival","carnival","party"],char:'🎪',fitzpatrick_scale:false,category:"activity"},woman_juggling:{keywords:["juggle","balance","skill","multitask"],char:'🤹‍♀️',fitzpatrick_scale:true,category:"activity"},man_juggling:{keywords:["juggle","balance","skill","multitask"],char:'🤹‍♂️',fitzpatrick_scale:true,category:"activity"},microphone:{keywords:["sound","music","PA","sing","talkshow"],char:'🎤',fitzpatrick_scale:false,category:"activity"},headphones:{keywords:["music","score","gadgets"],char:'🎧',fitzpatrick_scale:false,category:"activity"},musical_score:{keywords:["treble","clef","compose"],char:'🎼',fitzpatrick_scale:false,category:"activity"},musical_keyboard:{keywords:["piano","instrument","compose"],char:'🎹',fitzpatrick_scale:false,category:"activity"},drum:{keywords:["music","instrument","drumsticks","snare"],char:'🥁',fitzpatrick_scale:false,category:"activity"},saxophone:{keywords:["music","instrument","jazz","blues"],char:'🎷',fitzpatrick_scale:false,category:"activity"},trumpet:{keywords:["music","brass"],char:'🎺',fitzpatrick_scale:false,category:"activity"},guitar:{keywords:["music","instrument"],char:'🎸',fitzpatrick_scale:false,category:"activity"},violin:{keywords:["music","instrument","orchestra","symphony"],char:'🎻',fitzpatrick_scale:false,category:"activity"},clapper:{keywords:["movie","film","record"],char:'🎬',fitzpatrick_scale:false,category:"activity"},video_game:{keywords:["play","console","PS4","controller"],char:'🎮',fitzpatrick_scale:false,category:"activity"},space_invader:{keywords:["game","arcade","play"],char:'👾',fitzpatrick_scale:false,category:"activity"},dart:{keywords:["game","play","bar","target","bullseye"],char:'🎯',fitzpatrick_scale:false,category:"activity"},game_die:{keywords:["dice","random","tabletop","play","luck"],char:'🎲',fitzpatrick_scale:false,category:"activity"},chess_pawn:{keywords:["expendable"],char:"♟",fitzpatrick_scale:false,category:"activity"},slot_machine:{keywords:["bet","gamble","vegas","fruit machine","luck","casino"],char:'🎰',fitzpatrick_scale:false,category:"activity"},jigsaw:{keywords:["interlocking","puzzle","piece"],char:'🧩',fitzpatrick_scale:false,category:"activity"},bowling:{keywords:["sports","fun","play"],char:'🎳',fitzpatrick_scale:false,category:"activity"},red_car:{keywords:["red","transportation","vehicle"],char:'🚗',fitzpatrick_scale:false,category:"travel_and_places"},taxi:{keywords:["uber","vehicle","cars","transportation"],char:'🚕',fitzpatrick_scale:false,category:"travel_and_places"},blue_car:{keywords:["transportation","vehicle"],char:'🚙',fitzpatrick_scale:false,category:"travel_and_places"},bus:{keywords:["car","vehicle","transportation"],char:'🚌',fitzpatrick_scale:false,category:"travel_and_places"},trolleybus:{keywords:["bart","transportation","vehicle"],char:'🚎',fitzpatrick_scale:false,category:"travel_and_places"},racing_car:{keywords:["sports","race","fast","formula","f1"],char:'🏎',fitzpatrick_scale:false,category:"travel_and_places"},police_car:{keywords:["vehicle","cars","transportation","law","legal","enforcement"],char:'🚓',fitzpatrick_scale:false,category:"travel_and_places"},ambulance:{keywords:["health","911","hospital"],char:'🚑',fitzpatrick_scale:false,category:"travel_and_places"},fire_engine:{keywords:["transportation","cars","vehicle"],char:'🚒',fitzpatrick_scale:false,category:"travel_and_places"},minibus:{keywords:["vehicle","car","transportation"],char:'🚐',fitzpatrick_scale:false,category:"travel_and_places"},truck:{keywords:["cars","transportation"],char:'🚚',fitzpatrick_scale:false,category:"travel_and_places"},articulated_lorry:{keywords:["vehicle","cars","transportation","express"],char:'🚛',fitzpatrick_scale:false,category:"travel_and_places"},tractor:{keywords:["vehicle","car","farming","agriculture"],char:'🚜',fitzpatrick_scale:false,category:"travel_and_places"},kick_scooter:{keywords:["vehicle","kick","razor"],char:'🛴',fitzpatrick_scale:false,category:"travel_and_places"},motorcycle:{keywords:["race","sports","fast"],char:'🏍',fitzpatrick_scale:false,category:"travel_and_places"},bike:{keywords:["sports","bicycle","exercise","hipster"],char:'🚲',fitzpatrick_scale:false,category:"travel_and_places"},motor_scooter:{keywords:["vehicle","vespa","sasha"],char:'🛵',fitzpatrick_scale:false,category:"travel_and_places"},rotating_light:{keywords:["police","ambulance","911","emergency","alert","error","pinged","law","legal"],char:'🚨',fitzpatrick_scale:false,category:"travel_and_places"},oncoming_police_car:{keywords:["vehicle","law","legal","enforcement","911"],char:'🚔',fitzpatrick_scale:false,category:"travel_and_places"},oncoming_bus:{keywords:["vehicle","transportation"],char:'🚍',fitzpatrick_scale:false,category:"travel_and_places"},oncoming_automobile:{keywords:["car","vehicle","transportation"],char:'🚘',fitzpatrick_scale:false,category:"travel_and_places"},oncoming_taxi:{keywords:["vehicle","cars","uber"],char:'🚖',fitzpatrick_scale:false,category:"travel_and_places"},aerial_tramway:{keywords:["transportation","vehicle","ski"],char:'🚡',fitzpatrick_scale:false,category:"travel_and_places"},mountain_cableway:{keywords:["transportation","vehicle","ski"],char:'🚠',fitzpatrick_scale:false,category:"travel_and_places"},suspension_railway:{keywords:["vehicle","transportation"],char:'🚟',fitzpatrick_scale:false,category:"travel_and_places"},railway_car:{keywords:["transportation","vehicle"],char:'🚃',fitzpatrick_scale:false,category:"travel_and_places"},train:{keywords:["transportation","vehicle","carriage","public","travel"],char:'🚋',fitzpatrick_scale:false,category:"travel_and_places"},monorail:{keywords:["transportation","vehicle"],char:'🚝',fitzpatrick_scale:false,category:"travel_and_places"},bullettrain_side:{keywords:["transportation","vehicle"],char:'🚄',fitzpatrick_scale:false,category:"travel_and_places"},bullettrain_front:{keywords:["transportation","vehicle","speed","fast","public","travel"],char:'🚅',fitzpatrick_scale:false,category:"travel_and_places"},light_rail:{keywords:["transportation","vehicle"],char:'🚈',fitzpatrick_scale:false,category:"travel_and_places"},mountain_railway:{keywords:["transportation","vehicle"],char:'🚞',fitzpatrick_scale:false,category:"travel_and_places"},steam_locomotive:{keywords:["transportation","vehicle","train"],char:'🚂',fitzpatrick_scale:false,category:"travel_and_places"},train2:{keywords:["transportation","vehicle"],char:'🚆',fitzpatrick_scale:false,category:"travel_and_places"},metro:{keywords:["transportation","blue-square","mrt","underground","tube"],char:'🚇',fitzpatrick_scale:false,category:"travel_and_places"},tram:{keywords:["transportation","vehicle"],char:'🚊',fitzpatrick_scale:false,category:"travel_and_places"},station:{keywords:["transportation","vehicle","public"],char:'🚉',fitzpatrick_scale:false,category:"travel_and_places"},flying_saucer:{keywords:["transportation","vehicle","ufo"],char:'🛸',fitzpatrick_scale:false,category:"travel_and_places"},helicopter:{keywords:["transportation","vehicle","fly"],char:'🚁',fitzpatrick_scale:false,category:"travel_and_places"},small_airplane:{keywords:["flight","transportation","fly","vehicle"],char:'🛩',fitzpatrick_scale:false,category:"travel_and_places"},airplane:{keywords:["vehicle","transportation","flight","fly"],char:'✈️',fitzpatrick_scale:false,category:"travel_and_places"},flight_departure:{keywords:["airport","flight","landing"],char:'🛫',fitzpatrick_scale:false,category:"travel_and_places"},flight_arrival:{keywords:["airport","flight","boarding"],char:'🛬',fitzpatrick_scale:false,category:"travel_and_places"},sailboat:{keywords:["ship","summer","transportation","water","sailing"],char:'⛵',fitzpatrick_scale:false,category:"travel_and_places"},motor_boat:{keywords:["ship"],char:'🛥',fitzpatrick_scale:false,category:"travel_and_places"},speedboat:{keywords:["ship","transportation","vehicle","summer"],char:'🚤',fitzpatrick_scale:false,category:"travel_and_places"},ferry:{keywords:["boat","ship","yacht"],char:'⛴',fitzpatrick_scale:false,category:"travel_and_places"},passenger_ship:{keywords:["yacht","cruise","ferry"],char:'🛳',fitzpatrick_scale:false,category:"travel_and_places"},rocket:{keywords:["launch","ship","staffmode","NASA","outer space","outer_space","fly"],char:'🚀',fitzpatrick_scale:false,category:"travel_and_places"},artificial_satellite:{keywords:["communication","gps","orbit","spaceflight","NASA","ISS"],char:'🛰',fitzpatrick_scale:false,category:"travel_and_places"},seat:{keywords:["sit","airplane","transport","bus","flight","fly"],char:'💺',fitzpatrick_scale:false,category:"travel_and_places"},canoe:{keywords:["boat","paddle","water","ship"],char:'🛶',fitzpatrick_scale:false,category:"travel_and_places"},anchor:{keywords:["ship","ferry","sea","boat"],char:'⚓',fitzpatrick_scale:false,category:"travel_and_places"},construction:{keywords:["wip","progress","caution","warning"],char:'🚧',fitzpatrick_scale:false,category:"travel_and_places"},fuelpump:{keywords:["gas station","petroleum"],char:'⛽',fitzpatrick_scale:false,category:"travel_and_places"},busstop:{keywords:["transportation","wait"],char:'🚏',fitzpatrick_scale:false,category:"travel_and_places"},vertical_traffic_light:{keywords:["transportation","driving"],char:'🚦',fitzpatrick_scale:false,category:"travel_and_places"},traffic_light:{keywords:["transportation","signal"],char:'🚥',fitzpatrick_scale:false,category:"travel_and_places"},checkered_flag:{keywords:["contest","finishline","race","gokart"],char:'🏁',fitzpatrick_scale:false,category:"travel_and_places"},ship:{keywords:["transportation","titanic","deploy"],char:'🚢',fitzpatrick_scale:false,category:"travel_and_places"},ferris_wheel:{keywords:["photo","carnival","londoneye"],char:'🎡',fitzpatrick_scale:false,category:"travel_and_places"},roller_coaster:{keywords:["carnival","playground","photo","fun"],char:'🎢',fitzpatrick_scale:false,category:"travel_and_places"},carousel_horse:{keywords:["photo","carnival"],char:'🎠',fitzpatrick_scale:false,category:"travel_and_places"},building_construction:{keywords:["wip","working","progress"],char:'🏗',fitzpatrick_scale:false,category:"travel_and_places"},foggy:{keywords:["photo","mountain"],char:'🌁',fitzpatrick_scale:false,category:"travel_and_places"},tokyo_tower:{keywords:["photo","japanese"],char:'🗼',fitzpatrick_scale:false,category:"travel_and_places"},factory:{keywords:["building","industry","pollution","smoke"],char:'🏭',fitzpatrick_scale:false,category:"travel_and_places"},fountain:{keywords:["photo","summer","water","fresh"],char:'⛲',fitzpatrick_scale:false,category:"travel_and_places"},rice_scene:{keywords:["photo","japan","asia","tsukimi"],char:'🎑',fitzpatrick_scale:false,category:"travel_and_places"},mountain:{keywords:["photo","nature","environment"],char:'⛰',fitzpatrick_scale:false,category:"travel_and_places"},mountain_snow:{keywords:["photo","nature","environment","winter","cold"],char:'🏔',fitzpatrick_scale:false,category:"travel_and_places"},mount_fuji:{keywords:["photo","mountain","nature","japanese"],char:'🗻',fitzpatrick_scale:false,category:"travel_and_places"},volcano:{keywords:["photo","nature","disaster"],char:'🌋',fitzpatrick_scale:false,category:"travel_and_places"},japan:{keywords:["nation","country","japanese","asia"],char:'🗾',fitzpatrick_scale:false,category:"travel_and_places"},camping:{keywords:["photo","outdoors","tent"],char:'🏕',fitzpatrick_scale:false,category:"travel_and_places"},tent:{keywords:["photo","camping","outdoors"],char:'⛺',fitzpatrick_scale:false,category:"travel_and_places"},national_park:{keywords:["photo","environment","nature"],char:'🏞',fitzpatrick_scale:false,category:"travel_and_places"},motorway:{keywords:["road","cupertino","interstate","highway"],char:'🛣',fitzpatrick_scale:false,category:"travel_and_places"},railway_track:{keywords:["train","transportation"],char:'🛤',fitzpatrick_scale:false,category:"travel_and_places"},sunrise:{keywords:["morning","view","vacation","photo"],char:'🌅',fitzpatrick_scale:false,category:"travel_and_places"},sunrise_over_mountains:{keywords:["view","vacation","photo"],char:'🌄',fitzpatrick_scale:false,category:"travel_and_places"},desert:{keywords:["photo","warm","saharah"],char:'🏜',fitzpatrick_scale:false,category:"travel_and_places"},beach_umbrella:{keywords:["weather","summer","sunny","sand","mojito"],char:'🏖',fitzpatrick_scale:false,category:"travel_and_places"},desert_island:{keywords:["photo","tropical","mojito"],char:'🏝',fitzpatrick_scale:false,category:"travel_and_places"},city_sunrise:{keywords:["photo","good morning","dawn"],char:'🌇',fitzpatrick_scale:false,category:"travel_and_places"},city_sunset:{keywords:["photo","evening","sky","buildings"],char:'🌆',fitzpatrick_scale:false,category:"travel_and_places"},cityscape:{keywords:["photo","night life","urban"],char:'🏙',fitzpatrick_scale:false,category:"travel_and_places"},night_with_stars:{keywords:["evening","city","downtown"],char:'🌃',fitzpatrick_scale:false,category:"travel_and_places"},bridge_at_night:{keywords:["photo","sanfrancisco"],char:'🌉',fitzpatrick_scale:false,category:"travel_and_places"},milky_way:{keywords:["photo","space","stars"],char:'🌌',fitzpatrick_scale:false,category:"travel_and_places"},stars:{keywords:["night","photo"],char:'🌠',fitzpatrick_scale:false,category:"travel_and_places"},sparkler:{keywords:["stars","night","shine"],char:'🎇',fitzpatrick_scale:false,category:"travel_and_places"},fireworks:{keywords:["photo","festival","carnival","congratulations"],char:'🎆',fitzpatrick_scale:false,category:"travel_and_places"},rainbow:{keywords:["nature","happy","unicorn_face","photo","sky","spring"],char:'🌈',fitzpatrick_scale:false,category:"travel_and_places"},houses:{keywords:["buildings","photo"],char:'🏘',fitzpatrick_scale:false,category:"travel_and_places"},european_castle:{keywords:["building","royalty","history"],char:'🏰',fitzpatrick_scale:false,category:"travel_and_places"},japanese_castle:{keywords:["photo","building"],char:'🏯',fitzpatrick_scale:false,category:"travel_and_places"},stadium:{keywords:["photo","place","sports","concert","venue"],char:'🏟',fitzpatrick_scale:false,category:"travel_and_places"},statue_of_liberty:{keywords:["american","newyork"],char:'🗽',fitzpatrick_scale:false,category:"travel_and_places"},house:{keywords:["building","home"],char:'🏠',fitzpatrick_scale:false,category:"travel_and_places"},house_with_garden:{keywords:["home","plant","nature"],char:'🏡',fitzpatrick_scale:false,category:"travel_and_places"},derelict_house:{keywords:["abandon","evict","broken","building"],char:'🏚',fitzpatrick_scale:false,category:"travel_and_places"},office:{keywords:["building","bureau","work"],char:'🏢',fitzpatrick_scale:false,category:"travel_and_places"},department_store:{keywords:["building","shopping","mall"],char:'🏬',fitzpatrick_scale:false,category:"travel_and_places"},post_office:{keywords:["building","envelope","communication"],char:'🏣',fitzpatrick_scale:false,category:"travel_and_places"},european_post_office:{keywords:["building","email"],char:'🏤',fitzpatrick_scale:false,category:"travel_and_places"},hospital:{keywords:["building","health","surgery","doctor"],char:'🏥',fitzpatrick_scale:false,category:"travel_and_places"},bank:{keywords:["building","money","sales","cash","business","enterprise"],char:'🏦',fitzpatrick_scale:false,category:"travel_and_places"},hotel:{keywords:["building","accomodation","checkin"],char:'🏨',fitzpatrick_scale:false,category:"travel_and_places"},convenience_store:{keywords:["building","shopping","groceries"],char:'🏪',fitzpatrick_scale:false,category:"travel_and_places"},school:{keywords:["building","student","education","learn","teach"],char:'🏫',fitzpatrick_scale:false,category:"travel_and_places"},love_hotel:{keywords:["like","affection","dating"],char:'🏩',fitzpatrick_scale:false,category:"travel_and_places"},wedding:{keywords:["love","like","affection","couple","marriage","bride","groom"],char:'💒',fitzpatrick_scale:false,category:"travel_and_places"},classical_building:{keywords:["art","culture","history"],char:'🏛',fitzpatrick_scale:false,category:"travel_and_places"},church:{keywords:["building","religion","christ"],char:'⛪',fitzpatrick_scale:false,category:"travel_and_places"},mosque:{keywords:["islam","worship","minaret"],char:'🕌',fitzpatrick_scale:false,category:"travel_and_places"},synagogue:{keywords:["judaism","worship","temple","jewish"],char:'🕍',fitzpatrick_scale:false,category:"travel_and_places"},kaaba:{keywords:["mecca","mosque","islam"],char:'🕋',fitzpatrick_scale:false,category:"travel_and_places"},shinto_shrine:{keywords:["temple","japan","kyoto"],char:'⛩',fitzpatrick_scale:false,category:"travel_and_places"},watch:{keywords:["time","accessories"],char:'⌚',fitzpatrick_scale:false,category:"objects"},iphone:{keywords:["technology","apple","gadgets","dial"],char:'📱',fitzpatrick_scale:false,category:"objects"},calling:{keywords:["iphone","incoming"],char:'📲',fitzpatrick_scale:false,category:"objects"},computer:{keywords:["technology","laptop","screen","display","monitor"],char:'💻',fitzpatrick_scale:false,category:"objects"},keyboard:{keywords:["technology","computer","type","input","text"],char:'⌨',fitzpatrick_scale:false,category:"objects"},desktop_computer:{keywords:["technology","computing","screen"],char:'🖥',fitzpatrick_scale:false,category:"objects"},printer:{keywords:["paper","ink"],char:'🖨',fitzpatrick_scale:false,category:"objects"},computer_mouse:{keywords:["click"],char:'🖱',fitzpatrick_scale:false,category:"objects"},trackball:{keywords:["technology","trackpad"],char:'🖲',fitzpatrick_scale:false,category:"objects"},joystick:{keywords:["game","play"],char:'🕹',fitzpatrick_scale:false,category:"objects"},clamp:{keywords:["tool"],char:'🗜',fitzpatrick_scale:false,category:"objects"},minidisc:{keywords:["technology","record","data","disk","90s"],char:'💽',fitzpatrick_scale:false,category:"objects"},floppy_disk:{keywords:["oldschool","technology","save","90s","80s"],char:'💾',fitzpatrick_scale:false,category:"objects"},cd:{keywords:["technology","dvd","disk","disc","90s"],char:'💿',fitzpatrick_scale:false,category:"objects"},dvd:{keywords:["cd","disk","disc"],char:'📀',fitzpatrick_scale:false,category:"objects"},vhs:{keywords:["record","video","oldschool","90s","80s"],char:'📼',fitzpatrick_scale:false,category:"objects"},camera:{keywords:["gadgets","photography"],char:'📷',fitzpatrick_scale:false,category:"objects"},camera_flash:{keywords:["photography","gadgets"],char:'📸',fitzpatrick_scale:false,category:"objects"},video_camera:{keywords:["film","record"],char:'📹',fitzpatrick_scale:false,category:"objects"},movie_camera:{keywords:["film","record"],char:'🎥',fitzpatrick_scale:false,category:"objects"},film_projector:{keywords:["video","tape","record","movie"],char:'📽',fitzpatrick_scale:false,category:"objects"},film_strip:{keywords:["movie"],char:'🎞',fitzpatrick_scale:false,category:"objects"},telephone_receiver:{keywords:["technology","communication","dial"],char:'📞',fitzpatrick_scale:false,category:"objects"},phone:{keywords:["technology","communication","dial","telephone"],char:'☎️',fitzpatrick_scale:false,category:"objects"},pager:{keywords:["bbcall","oldschool","90s"],char:'📟',fitzpatrick_scale:false,category:"objects"},fax:{keywords:["communication","technology"],char:'📠',fitzpatrick_scale:false,category:"objects"},tv:{keywords:["technology","program","oldschool","show","television"],char:'📺',fitzpatrick_scale:false,category:"objects"},radio:{keywords:["communication","music","podcast","program"],char:'📻',fitzpatrick_scale:false,category:"objects"},studio_microphone:{keywords:["sing","recording","artist","talkshow"],char:'🎙',fitzpatrick_scale:false,category:"objects"},level_slider:{keywords:["scale"],char:'🎚',fitzpatrick_scale:false,category:"objects"},control_knobs:{keywords:["dial"],char:'🎛',fitzpatrick_scale:false,category:"objects"},compass:{keywords:["magnetic","navigation","orienteering"],char:'🧭',fitzpatrick_scale:false,category:"objects"},stopwatch:{keywords:["time","deadline"],char:'⏱',fitzpatrick_scale:false,category:"objects"},timer_clock:{keywords:["alarm"],char:'⏲',fitzpatrick_scale:false,category:"objects"},alarm_clock:{keywords:["time","wake"],char:'⏰',fitzpatrick_scale:false,category:"objects"},mantelpiece_clock:{keywords:["time"],char:'🕰',fitzpatrick_scale:false,category:"objects"},hourglass_flowing_sand:{keywords:["oldschool","time","countdown"],char:'⏳',fitzpatrick_scale:false,category:"objects"},hourglass:{keywords:["time","clock","oldschool","limit","exam","quiz","test"],char:'⌛',fitzpatrick_scale:false,category:"objects"},satellite:{keywords:["communication","future","radio","space"],char:'📡',fitzpatrick_scale:false,category:"objects"},battery:{keywords:["power","energy","sustain"],char:'🔋',fitzpatrick_scale:false,category:"objects"},electric_plug:{keywords:["charger","power"],char:'🔌',fitzpatrick_scale:false,category:"objects"},bulb:{keywords:["light","electricity","idea"],char:'💡',fitzpatrick_scale:false,category:"objects"},flashlight:{keywords:["dark","camping","sight","night"],char:'🔦',fitzpatrick_scale:false,category:"objects"},candle:{keywords:["fire","wax"],char:'🕯',fitzpatrick_scale:false,category:"objects"},fire_extinguisher:{keywords:["quench"],char:'🧯',fitzpatrick_scale:false,category:"objects"},wastebasket:{keywords:["bin","trash","rubbish","garbage","toss"],char:'🗑',fitzpatrick_scale:false,category:"objects"},oil_drum:{keywords:["barrell"],char:'🛢',fitzpatrick_scale:false,category:"objects"},money_with_wings:{keywords:["dollar","bills","payment","sale"],char:'💸',fitzpatrick_scale:false,category:"objects"},dollar:{keywords:["money","sales","bill","currency"],char:'💵',fitzpatrick_scale:false,category:"objects"},yen:{keywords:["money","sales","japanese","dollar","currency"],char:'💴',fitzpatrick_scale:false,category:"objects"},euro:{keywords:["money","sales","dollar","currency"],char:'💶',fitzpatrick_scale:false,category:"objects"},pound:{keywords:["british","sterling","money","sales","bills","uk","england","currency"],char:'💷',fitzpatrick_scale:false,category:"objects"},moneybag:{keywords:["dollar","payment","coins","sale"],char:'💰',fitzpatrick_scale:false,category:"objects"},credit_card:{keywords:["money","sales","dollar","bill","payment","shopping"],char:'💳',fitzpatrick_scale:false,category:"objects"},gem:{keywords:["blue","ruby","diamond","jewelry"],char:'💎',fitzpatrick_scale:false,category:"objects"},balance_scale:{keywords:["law","fairness","weight"],char:'⚖',fitzpatrick_scale:false,category:"objects"},toolbox:{keywords:["tools","diy","fix","maintainer","mechanic"],char:'🧰',fitzpatrick_scale:false,category:"objects"},wrench:{keywords:["tools","diy","ikea","fix","maintainer"],char:'🔧',fitzpatrick_scale:false,category:"objects"},hammer:{keywords:["tools","build","create"],char:'🔨',fitzpatrick_scale:false,category:"objects"},hammer_and_pick:{keywords:["tools","build","create"],char:'⚒',fitzpatrick_scale:false,category:"objects"},hammer_and_wrench:{keywords:["tools","build","create"],char:'🛠',fitzpatrick_scale:false,category:"objects"},pick:{keywords:["tools","dig"],char:'⛏',fitzpatrick_scale:false,category:"objects"},nut_and_bolt:{keywords:["handy","tools","fix"],char:'🔩',fitzpatrick_scale:false,category:"objects"},gear:{keywords:["cog"],char:'⚙',fitzpatrick_scale:false,category:"objects"},brick:{keywords:["bricks"],char:'🧱',fitzpatrick_scale:false,category:"objects"},chains:{keywords:["lock","arrest"],char:'⛓',fitzpatrick_scale:false,category:"objects"},magnet:{keywords:["attraction","magnetic"],char:'🧲',fitzpatrick_scale:false,category:"objects"},gun:{keywords:["violence","weapon","pistol","revolver"],char:'🔫',fitzpatrick_scale:false,category:"objects"},bomb:{keywords:["boom","explode","explosion","terrorism"],char:'💣',fitzpatrick_scale:false,category:"objects"},firecracker:{keywords:["dynamite","boom","explode","explosion","explosive"],char:'🧨',fitzpatrick_scale:false,category:"objects"},hocho:{keywords:["knife","blade","cutlery","kitchen","weapon"],char:'🔪',fitzpatrick_scale:false,category:"objects"},dagger:{keywords:["weapon"],char:'🗡',fitzpatrick_scale:false,category:"objects"},crossed_swords:{keywords:["weapon"],char:'⚔',fitzpatrick_scale:false,category:"objects"},shield:{keywords:["protection","security"],char:'🛡',fitzpatrick_scale:false,category:"objects"},smoking:{keywords:["kills","tobacco","cigarette","joint","smoke"],char:'🚬',fitzpatrick_scale:false,category:"objects"},skull_and_crossbones:{keywords:["poison","danger","deadly","scary","death","pirate","evil"],char:'☠',fitzpatrick_scale:false,category:"objects"},coffin:{keywords:["vampire","dead","die","death","rip","graveyard","cemetery","casket","funeral","box"],char:'⚰',fitzpatrick_scale:false,category:"objects"},funeral_urn:{keywords:["dead","die","death","rip","ashes"],char:'⚱',fitzpatrick_scale:false,category:"objects"},amphora:{keywords:["vase","jar"],char:'🏺',fitzpatrick_scale:false,category:"objects"},crystal_ball:{keywords:["disco","party","magic","circus","fortune_teller"],char:'🔮',fitzpatrick_scale:false,category:"objects"},prayer_beads:{keywords:["dhikr","religious"],char:'📿',fitzpatrick_scale:false,category:"objects"},nazar_amulet:{keywords:["bead","charm"],char:'🧿',fitzpatrick_scale:false,category:"objects"},barber:{keywords:["hair","salon","style"],char:'💈',fitzpatrick_scale:false,category:"objects"},alembic:{keywords:["distilling","science","experiment","chemistry"],char:'⚗',fitzpatrick_scale:false,category:"objects"},telescope:{keywords:["stars","space","zoom","science","astronomy"],char:'🔭',fitzpatrick_scale:false,category:"objects"},microscope:{keywords:["laboratory","experiment","zoomin","science","study"],char:'🔬',fitzpatrick_scale:false,category:"objects"},hole:{keywords:["embarrassing"],char:'🕳',fitzpatrick_scale:false,category:"objects"},pill:{keywords:["health","medicine","doctor","pharmacy","drug"],char:'💊',fitzpatrick_scale:false,category:"objects"},syringe:{keywords:["health","hospital","drugs","blood","medicine","needle","doctor","nurse"],char:'💉',fitzpatrick_scale:false,category:"objects"},dna:{keywords:["biologist","genetics","life"],char:'🧬',fitzpatrick_scale:false,category:"objects"},microbe:{keywords:["amoeba","bacteria","germs"],char:'🦠',fitzpatrick_scale:false,category:"objects"},petri_dish:{keywords:["bacteria","biology","culture","lab"],char:'🧫',fitzpatrick_scale:false,category:"objects"},test_tube:{keywords:["chemistry","experiment","lab","science"],char:'🧪',fitzpatrick_scale:false,category:"objects"},thermometer:{keywords:["weather","temperature","hot","cold"],char:'🌡',fitzpatrick_scale:false,category:"objects"},broom:{keywords:["cleaning","sweeping","witch"],char:'🧹',fitzpatrick_scale:false,category:"objects"},basket:{keywords:["laundry"],char:'🧺',fitzpatrick_scale:false,category:"objects"},toilet_paper:{keywords:["roll"],char:'🧻',fitzpatrick_scale:false,category:"objects"},label:{keywords:["sale","tag"],char:'🏷',fitzpatrick_scale:false,category:"objects"},bookmark:{keywords:["favorite","label","save"],char:'🔖',fitzpatrick_scale:false,category:"objects"},toilet:{keywords:["restroom","wc","washroom","bathroom","potty"],char:'🚽',fitzpatrick_scale:false,category:"objects"},shower:{keywords:["clean","water","bathroom"],char:'🚿',fitzpatrick_scale:false,category:"objects"},bathtub:{keywords:["clean","shower","bathroom"],char:'🛁',fitzpatrick_scale:false,category:"objects"},soap:{keywords:["bar","bathing","cleaning","lather"],char:'🧼',fitzpatrick_scale:false,category:"objects"},sponge:{keywords:["absorbing","cleaning","porous"],char:'🧽',fitzpatrick_scale:false,category:"objects"},lotion_bottle:{keywords:["moisturizer","sunscreen"],char:'🧴',fitzpatrick_scale:false,category:"objects"},key:{keywords:["lock","door","password"],char:'🔑',fitzpatrick_scale:false,category:"objects"},old_key:{keywords:["lock","door","password"],char:'🗝',fitzpatrick_scale:false,category:"objects"},couch_and_lamp:{keywords:["read","chill"],char:'🛋',fitzpatrick_scale:false,category:"objects"},sleeping_bed:{keywords:["bed","rest"],char:'🛌',fitzpatrick_scale:true,category:"objects"},bed:{keywords:["sleep","rest"],char:'🛏',fitzpatrick_scale:false,category:"objects"},door:{keywords:["house","entry","exit"],char:'🚪',fitzpatrick_scale:false,category:"objects"},bellhop_bell:{keywords:["service"],char:'🛎',fitzpatrick_scale:false,category:"objects"},teddy_bear:{keywords:["plush","stuffed"],char:'🧸',fitzpatrick_scale:false,category:"objects"},framed_picture:{keywords:["photography"],char:'🖼',fitzpatrick_scale:false,category:"objects"},world_map:{keywords:["location","direction"],char:'🗺',fitzpatrick_scale:false,category:"objects"},parasol_on_ground:{keywords:["weather","summer"],char:'⛱',fitzpatrick_scale:false,category:"objects"},moyai:{keywords:["rock","easter island","moai"],char:'🗿',fitzpatrick_scale:false,category:"objects"},shopping:{keywords:["mall","buy","purchase"],char:'🛍',fitzpatrick_scale:false,category:"objects"},shopping_cart:{keywords:["trolley"],char:'🛒',fitzpatrick_scale:false,category:"objects"},balloon:{keywords:["party","celebration","birthday","circus"],char:'🎈',fitzpatrick_scale:false,category:"objects"},flags:{keywords:["fish","japanese","koinobori","carp","banner"],char:'🎏',fitzpatrick_scale:false,category:"objects"},ribbon:{keywords:["decoration","pink","girl","bowtie"],char:'🎀',fitzpatrick_scale:false,category:"objects"},gift:{keywords:["present","birthday","christmas","xmas"],char:'🎁',fitzpatrick_scale:false,category:"objects"},confetti_ball:{keywords:["festival","party","birthday","circus"],char:'🎊',fitzpatrick_scale:false,category:"objects"},tada:{keywords:["party","congratulations","birthday","magic","circus","celebration"],char:'🎉',fitzpatrick_scale:false,category:"objects"},dolls:{keywords:["japanese","toy","kimono"],char:'🎎',fitzpatrick_scale:false,category:"objects"},wind_chime:{keywords:["nature","ding","spring","bell"],char:'🎐',fitzpatrick_scale:false,category:"objects"},crossed_flags:{keywords:["japanese","nation","country","border"],char:'🎌',fitzpatrick_scale:false,category:"objects"},izakaya_lantern:{keywords:["light","paper","halloween","spooky"],char:'🏮',fitzpatrick_scale:false,category:"objects"},red_envelope:{keywords:["gift"],char:'🧧',fitzpatrick_scale:false,category:"objects"},email:{keywords:["letter","postal","inbox","communication"],char:'✉️',fitzpatrick_scale:false,category:"objects"},envelope_with_arrow:{keywords:["email","communication"],char:'📩',fitzpatrick_scale:false,category:"objects"},incoming_envelope:{keywords:["email","inbox"],char:'📨',fitzpatrick_scale:false,category:"objects"},"e-mail":{keywords:["communication","inbox"],char:'📧',fitzpatrick_scale:false,category:"objects"},love_letter:{keywords:["email","like","affection","envelope","valentines"],char:'💌',fitzpatrick_scale:false,category:"objects"},postbox:{keywords:["email","letter","envelope"],char:'📮',fitzpatrick_scale:false,category:"objects"},mailbox_closed:{keywords:["email","communication","inbox"],char:'📪',fitzpatrick_scale:false,category:"objects"},mailbox:{keywords:["email","inbox","communication"],char:'📫',fitzpatrick_scale:false,category:"objects"},mailbox_with_mail:{keywords:["email","inbox","communication"],char:'📬',fitzpatrick_scale:false,category:"objects"},mailbox_with_no_mail:{keywords:["email","inbox"],char:'📭',fitzpatrick_scale:false,category:"objects"},package:{keywords:["mail","gift","cardboard","box","moving"],char:'📦',fitzpatrick_scale:false,category:"objects"},postal_horn:{keywords:["instrument","music"],char:'📯',fitzpatrick_scale:false,category:"objects"},inbox_tray:{keywords:["email","documents"],char:'📥',fitzpatrick_scale:false,category:"objects"},outbox_tray:{keywords:["inbox","email"],char:'📤',fitzpatrick_scale:false,category:"objects"},scroll:{keywords:["documents","ancient","history","paper"],char:'📜',fitzpatrick_scale:false,category:"objects"},page_with_curl:{keywords:["documents","office","paper"],char:'📃',fitzpatrick_scale:false,category:"objects"},bookmark_tabs:{keywords:["favorite","save","order","tidy"],char:'📑',fitzpatrick_scale:false,category:"objects"},receipt:{keywords:["accounting","expenses"],char:'🧾',fitzpatrick_scale:false,category:"objects"},bar_chart:{keywords:["graph","presentation","stats"],char:'📊',fitzpatrick_scale:false,category:"objects"},chart_with_upwards_trend:{keywords:["graph","presentation","stats","recovery","business","economics","money","sales","good","success"],char:'📈',fitzpatrick_scale:false,category:"objects"},chart_with_downwards_trend:{keywords:["graph","presentation","stats","recession","business","economics","money","sales","bad","failure"],char:'📉',fitzpatrick_scale:false,category:"objects"},page_facing_up:{keywords:["documents","office","paper","information"],char:'📄',fitzpatrick_scale:false,category:"objects"},date:{keywords:["calendar","schedule"],char:'📅',fitzpatrick_scale:false,category:"objects"},calendar:{keywords:["schedule","date","planning"],char:'📆',fitzpatrick_scale:false,category:"objects"},spiral_calendar:{keywords:["date","schedule","planning"],char:'🗓',fitzpatrick_scale:false,category:"objects"},card_index:{keywords:["business","stationery"],char:'📇',fitzpatrick_scale:false,category:"objects"},card_file_box:{keywords:["business","stationery"],char:'🗃',fitzpatrick_scale:false,category:"objects"},ballot_box:{keywords:["election","vote"],char:'🗳',fitzpatrick_scale:false,category:"objects"},file_cabinet:{keywords:["filing","organizing"],char:'🗄',fitzpatrick_scale:false,category:"objects"},clipboard:{keywords:["stationery","documents"],char:'📋',fitzpatrick_scale:false,category:"objects"},spiral_notepad:{keywords:["memo","stationery"],char:'🗒',fitzpatrick_scale:false,category:"objects"},file_folder:{keywords:["documents","business","office"],char:'📁',fitzpatrick_scale:false,category:"objects"},open_file_folder:{keywords:["documents","load"],char:'📂',fitzpatrick_scale:false,category:"objects"},card_index_dividers:{keywords:["organizing","business","stationery"],char:'🗂',fitzpatrick_scale:false,category:"objects"},newspaper_roll:{keywords:["press","headline"],char:'🗞',fitzpatrick_scale:false,category:"objects"},newspaper:{keywords:["press","headline"],char:'📰',fitzpatrick_scale:false,category:"objects"},notebook:{keywords:["stationery","record","notes","paper","study"],char:'📓',fitzpatrick_scale:false,category:"objects"},closed_book:{keywords:["read","library","knowledge","textbook","learn"],char:'📕',fitzpatrick_scale:false,category:"objects"},green_book:{keywords:["read","library","knowledge","study"],char:'📗',fitzpatrick_scale:false,category:"objects"},blue_book:{keywords:["read","library","knowledge","learn","study"],char:'📘',fitzpatrick_scale:false,category:"objects"},orange_book:{keywords:["read","library","knowledge","textbook","study"],char:'📙',fitzpatrick_scale:false,category:"objects"},notebook_with_decorative_cover:{keywords:["classroom","notes","record","paper","study"],char:'📔',fitzpatrick_scale:false,category:"objects"},ledger:{keywords:["notes","paper"],char:'📒',fitzpatrick_scale:false,category:"objects"},books:{keywords:["literature","library","study"],char:'📚',fitzpatrick_scale:false,category:"objects"},open_book:{keywords:["book","read","library","knowledge","literature","learn","study"],char:'📖',fitzpatrick_scale:false,category:"objects"},safety_pin:{keywords:["diaper"],char:'🧷',fitzpatrick_scale:false,category:"objects"},link:{keywords:["rings","url"],char:'🔗',fitzpatrick_scale:false,category:"objects"},paperclip:{keywords:["documents","stationery"],char:'📎',fitzpatrick_scale:false,category:"objects"},paperclips:{keywords:["documents","stationery"],char:'🖇',fitzpatrick_scale:false,category:"objects"},scissors:{keywords:["stationery","cut"],char:'✂️',fitzpatrick_scale:false,category:"objects"},triangular_ruler:{keywords:["stationery","math","architect","sketch"],char:'📐',fitzpatrick_scale:false,category:"objects"},straight_ruler:{keywords:["stationery","calculate","length","math","school","drawing","architect","sketch"],char:'📏',fitzpatrick_scale:false,category:"objects"},abacus:{keywords:["calculation"],char:'🧮',fitzpatrick_scale:false,category:"objects"},pushpin:{keywords:["stationery","mark","here"],char:'📌',fitzpatrick_scale:false,category:"objects"},round_pushpin:{keywords:["stationery","location","map","here"],char:'📍',fitzpatrick_scale:false,category:"objects"},triangular_flag_on_post:{keywords:["mark","milestone","place"],char:'🚩',fitzpatrick_scale:false,category:"objects"},white_flag:{keywords:["losing","loser","lost","surrender","give up","fail"],char:'🏳',fitzpatrick_scale:false,category:"objects"},black_flag:{keywords:["pirate"],char:'🏴',fitzpatrick_scale:false,category:"objects"},rainbow_flag:{keywords:["flag","rainbow","pride","gay","lgbt","glbt","queer","homosexual","lesbian","bisexual","transgender"],char:'🏳️‍🌈',fitzpatrick_scale:false,category:"objects"},closed_lock_with_key:{keywords:["security","privacy"],char:'🔐',fitzpatrick_scale:false,category:"objects"},lock:{keywords:["security","password","padlock"],char:'🔒',fitzpatrick_scale:false,category:"objects"},unlock:{keywords:["privacy","security"],char:'🔓',fitzpatrick_scale:false,category:"objects"},lock_with_ink_pen:{keywords:["security","secret"],char:'🔏',fitzpatrick_scale:false,category:"objects"},pen:{keywords:["stationery","writing","write"],char:'🖊',fitzpatrick_scale:false,category:"objects"},fountain_pen:{keywords:["stationery","writing","write"],char:'🖋',fitzpatrick_scale:false,category:"objects"},black_nib:{keywords:["pen","stationery","writing","write"],char:'✒️',fitzpatrick_scale:false,category:"objects"},memo:{keywords:["write","documents","stationery","pencil","paper","writing","legal","exam","quiz","test","study","compose"],char:'📝',fitzpatrick_scale:false,category:"objects"},pencil2:{keywords:["stationery","write","paper","writing","school","study"],char:'✏️',fitzpatrick_scale:false,category:"objects"},crayon:{keywords:["drawing","creativity"],char:'🖍',fitzpatrick_scale:false,category:"objects"},paintbrush:{keywords:["drawing","creativity","art"],char:'🖌',fitzpatrick_scale:false,category:"objects"},mag:{keywords:["search","zoom","find","detective"],char:'🔍',fitzpatrick_scale:false,category:"objects"},mag_right:{keywords:["search","zoom","find","detective"],char:'🔎',fitzpatrick_scale:false,category:"objects"},heart:{keywords:["love","like","valentines"],char:'❤️',fitzpatrick_scale:false,category:"symbols"},orange_heart:{keywords:["love","like","affection","valentines"],char:'🧡',fitzpatrick_scale:false,category:"symbols"},yellow_heart:{keywords:["love","like","affection","valentines"],char:'💛',fitzpatrick_scale:false,category:"symbols"},green_heart:{keywords:["love","like","affection","valentines"],char:'💚',fitzpatrick_scale:false,category:"symbols"},blue_heart:{keywords:["love","like","affection","valentines"],char:'💙',fitzpatrick_scale:false,category:"symbols"},purple_heart:{keywords:["love","like","affection","valentines"],char:'💜',fitzpatrick_scale:false,category:"symbols"},black_heart:{keywords:["evil"],char:'🖤',fitzpatrick_scale:false,category:"symbols"},broken_heart:{keywords:["sad","sorry","break","heart","heartbreak"],char:'💔',fitzpatrick_scale:false,category:"symbols"},heavy_heart_exclamation:{keywords:["decoration","love"],char:'❣',fitzpatrick_scale:false,category:"symbols"},two_hearts:{keywords:["love","like","affection","valentines","heart"],char:'💕',fitzpatrick_scale:false,category:"symbols"},revolving_hearts:{keywords:["love","like","affection","valentines"],char:'💞',fitzpatrick_scale:false,category:"symbols"},heartbeat:{keywords:["love","like","affection","valentines","pink","heart"],char:'💓',fitzpatrick_scale:false,category:"symbols"},heartpulse:{keywords:["like","love","affection","valentines","pink"],char:'💗',fitzpatrick_scale:false,category:"symbols"},sparkling_heart:{keywords:["love","like","affection","valentines"],char:'💖',fitzpatrick_scale:false,category:"symbols"},cupid:{keywords:["love","like","heart","affection","valentines"],char:'💘',fitzpatrick_scale:false,category:"symbols"},gift_heart:{keywords:["love","valentines"],char:'💝',fitzpatrick_scale:false,category:"symbols"},heart_decoration:{keywords:["purple-square","love","like"],char:'💟',fitzpatrick_scale:false,category:"symbols"},peace_symbol:{keywords:["hippie"],char:'☮',fitzpatrick_scale:false,category:"symbols"},latin_cross:{keywords:["christianity"],char:'✝',fitzpatrick_scale:false,category:"symbols"},star_and_crescent:{keywords:["islam"],char:'☪',fitzpatrick_scale:false,category:"symbols"},om:{keywords:["hinduism","buddhism","sikhism","jainism"],char:'🕉',fitzpatrick_scale:false,category:"symbols"},wheel_of_dharma:{keywords:["hinduism","buddhism","sikhism","jainism"],char:'☸',fitzpatrick_scale:false,category:"symbols"},star_of_david:{keywords:["judaism"],char:'✡',fitzpatrick_scale:false,category:"symbols"},six_pointed_star:{keywords:["purple-square","religion","jewish","hexagram"],char:'🔯',fitzpatrick_scale:false,category:"symbols"},menorah:{keywords:["hanukkah","candles","jewish"],char:'🕎',fitzpatrick_scale:false,category:"symbols"},yin_yang:{keywords:["balance"],char:'☯',fitzpatrick_scale:false,category:"symbols"},orthodox_cross:{keywords:["suppedaneum","religion"],char:'☦',fitzpatrick_scale:false,category:"symbols"},place_of_worship:{keywords:["religion","church","temple","prayer"],char:'🛐',fitzpatrick_scale:false,category:"symbols"},ophiuchus:{keywords:["sign","purple-square","constellation","astrology"],char:'⛎',fitzpatrick_scale:false,category:"symbols"},aries:{keywords:["sign","purple-square","zodiac","astrology"],char:'♈',fitzpatrick_scale:false,category:"symbols"},taurus:{keywords:["purple-square","sign","zodiac","astrology"],char:'♉',fitzpatrick_scale:false,category:"symbols"},gemini:{keywords:["sign","zodiac","purple-square","astrology"],char:'♊',fitzpatrick_scale:false,category:"symbols"},cancer:{keywords:["sign","zodiac","purple-square","astrology"],char:'♋',fitzpatrick_scale:false,category:"symbols"},leo:{keywords:["sign","purple-square","zodiac","astrology"],char:'♌',fitzpatrick_scale:false,category:"symbols"},virgo:{keywords:["sign","zodiac","purple-square","astrology"],char:'♍',fitzpatrick_scale:false,category:"symbols"},libra:{keywords:["sign","purple-square","zodiac","astrology"],char:'♎',fitzpatrick_scale:false,category:"symbols"},scorpius:{keywords:["sign","zodiac","purple-square","astrology","scorpio"],char:'♏',fitzpatrick_scale:false,category:"symbols"},sagittarius:{keywords:["sign","zodiac","purple-square","astrology"],char:'♐',fitzpatrick_scale:false,category:"symbols"},capricorn:{keywords:["sign","zodiac","purple-square","astrology"],char:'♑',fitzpatrick_scale:false,category:"symbols"},aquarius:{keywords:["sign","purple-square","zodiac","astrology"],char:'♒',fitzpatrick_scale:false,category:"symbols"},pisces:{keywords:["purple-square","sign","zodiac","astrology"],char:'♓',fitzpatrick_scale:false,category:"symbols"},id:{keywords:["purple-square","words"],char:'🆔',fitzpatrick_scale:false,category:"symbols"},atom_symbol:{keywords:["science","physics","chemistry"],char:'⚛',fitzpatrick_scale:false,category:"symbols"},u7a7a:{keywords:["kanji","japanese","chinese","empty","sky","blue-square"],char:'🈳',fitzpatrick_scale:false,category:"symbols"},u5272:{keywords:["cut","divide","chinese","kanji","pink-square"],char:'🈹',fitzpatrick_scale:false,category:"symbols"},radioactive:{keywords:["nuclear","danger"],char:'☢',fitzpatrick_scale:false,category:"symbols"},biohazard:{keywords:["danger"],char:'☣',fitzpatrick_scale:false,category:"symbols"},mobile_phone_off:{keywords:["mute","orange-square","silence","quiet"],char:'📴',fitzpatrick_scale:false,category:"symbols"},vibration_mode:{keywords:["orange-square","phone"],char:'📳',fitzpatrick_scale:false,category:"symbols"},u6709:{keywords:["orange-square","chinese","have","kanji"],char:'🈶',fitzpatrick_scale:false,category:"symbols"},u7121:{keywords:["nothing","chinese","kanji","japanese","orange-square"],char:'🈚',fitzpatrick_scale:false,category:"symbols"},u7533:{keywords:["chinese","japanese","kanji","orange-square"],char:'🈸',fitzpatrick_scale:false,category:"symbols"},u55b6:{keywords:["japanese","opening hours","orange-square"],char:'🈺',fitzpatrick_scale:false,category:"symbols"},u6708:{keywords:["chinese","month","moon","japanese","orange-square","kanji"],char:'🈷️',fitzpatrick_scale:false,category:"symbols"},eight_pointed_black_star:{keywords:["orange-square","shape","polygon"],char:'✴️',fitzpatrick_scale:false,category:"symbols"},vs:{keywords:["words","orange-square"],char:'🆚',fitzpatrick_scale:false,category:"symbols"},accept:{keywords:["ok","good","chinese","kanji","agree","yes","orange-circle"],char:'🉑',fitzpatrick_scale:false,category:"symbols"},white_flower:{keywords:["japanese","spring"],char:'💮',fitzpatrick_scale:false,category:"symbols"},ideograph_advantage:{keywords:["chinese","kanji","obtain","get","circle"],char:'🉐',fitzpatrick_scale:false,category:"symbols"},secret:{keywords:["privacy","chinese","sshh","kanji","red-circle"],char:'㊙️',fitzpatrick_scale:false,category:"symbols"},congratulations:{keywords:["chinese","kanji","japanese","red-circle"],char:'㊗️',fitzpatrick_scale:false,category:"symbols"},u5408:{keywords:["japanese","chinese","join","kanji","red-square"],char:'🈴',fitzpatrick_scale:false,category:"symbols"},u6e80:{keywords:["full","chinese","japanese","red-square","kanji"],char:'🈵',fitzpatrick_scale:false,category:"symbols"},u7981:{keywords:["kanji","japanese","chinese","forbidden","limit","restricted","red-square"],char:'🈲',fitzpatrick_scale:false,category:"symbols"},a:{keywords:["red-square","alphabet","letter"],char:'🅰️',fitzpatrick_scale:false,category:"symbols"},b:{keywords:["red-square","alphabet","letter"],char:'🅱️',fitzpatrick_scale:false,category:"symbols"},ab:{keywords:["red-square","alphabet"],char:'🆎',fitzpatrick_scale:false,category:"symbols"},cl:{keywords:["alphabet","words","red-square"],char:'🆑',fitzpatrick_scale:false,category:"symbols"},o2:{keywords:["alphabet","red-square","letter"],char:'🅾️',fitzpatrick_scale:false,category:"symbols"},sos:{keywords:["help","red-square","words","emergency","911"],char:'🆘',fitzpatrick_scale:false,category:"symbols"},no_entry:{keywords:["limit","security","privacy","bad","denied","stop","circle"],char:'⛔',fitzpatrick_scale:false,category:"symbols"},name_badge:{keywords:["fire","forbid"],char:'📛',fitzpatrick_scale:false,category:"symbols"},no_entry_sign:{keywords:["forbid","stop","limit","denied","disallow","circle"],char:'🚫',fitzpatrick_scale:false,category:"symbols"},x:{keywords:["no","delete","remove","cancel","red"],char:'❌',fitzpatrick_scale:false,category:"symbols"},o:{keywords:["circle","round"],char:'⭕',fitzpatrick_scale:false,category:"symbols"},stop_sign:{keywords:["stop"],char:'🛑',fitzpatrick_scale:false,category:"symbols"},anger:{keywords:["angry","mad"],char:'💢',fitzpatrick_scale:false,category:"symbols"},hotsprings:{keywords:["bath","warm","relax"],char:'♨️',fitzpatrick_scale:false,category:"symbols"},no_pedestrians:{keywords:["rules","crossing","walking","circle"],char:'🚷',fitzpatrick_scale:false,category:"symbols"},do_not_litter:{keywords:["trash","bin","garbage","circle"],char:'🚯',fitzpatrick_scale:false,category:"symbols"},no_bicycles:{keywords:["cyclist","prohibited","circle"],char:'🚳',fitzpatrick_scale:false,category:"symbols"},"non-potable_water":{keywords:["drink","faucet","tap","circle"],char:'🚱',fitzpatrick_scale:false,category:"symbols"},underage:{keywords:["18","drink","pub","night","minor","circle"],char:'🔞',fitzpatrick_scale:false,category:"symbols"},no_mobile_phones:{keywords:["iphone","mute","circle"],char:'📵',fitzpatrick_scale:false,category:"symbols"},exclamation:{keywords:["heavy_exclamation_mark","danger","surprise","punctuation","wow","warning"],char:'❗',fitzpatrick_scale:false,category:"symbols"},grey_exclamation:{keywords:["surprise","punctuation","gray","wow","warning"],char:'❕',fitzpatrick_scale:false,category:"symbols"},question:{keywords:["doubt","confused"],char:'❓',fitzpatrick_scale:false,category:"symbols"},grey_question:{keywords:["doubts","gray","huh","confused"],char:'❔',fitzpatrick_scale:false,category:"symbols"},bangbang:{keywords:["exclamation","surprise"],char:'‼️',fitzpatrick_scale:false,category:"symbols"},interrobang:{keywords:["wat","punctuation","surprise"],char:'⁉️',fitzpatrick_scale:false,category:"symbols"},low_brightness:{keywords:["sun","afternoon","warm","summer"],char:'🔅',fitzpatrick_scale:false,category:"symbols"},high_brightness:{keywords:["sun","light"],char:'🔆',fitzpatrick_scale:false,category:"symbols"},trident:{keywords:["weapon","spear"],char:'🔱',fitzpatrick_scale:false,category:"symbols"},fleur_de_lis:{keywords:["decorative","scout"],char:'⚜',fitzpatrick_scale:false,category:"symbols"},part_alternation_mark:{keywords:["graph","presentation","stats","business","economics","bad"],char:'〽️',fitzpatrick_scale:false,category:"symbols"},warning:{keywords:["exclamation","wip","alert","error","problem","issue"],char:'⚠️',fitzpatrick_scale:false,category:"symbols"},children_crossing:{keywords:["school","warning","danger","sign","driving","yellow-diamond"],char:'🚸',fitzpatrick_scale:false,category:"symbols"},beginner:{keywords:["badge","shield"],char:'🔰',fitzpatrick_scale:false,category:"symbols"},recycle:{keywords:["arrow","environment","garbage","trash"],char:'♻️',fitzpatrick_scale:false,category:"symbols"},u6307:{keywords:["chinese","point","green-square","kanji"],char:'🈯',fitzpatrick_scale:false,category:"symbols"},chart:{keywords:["green-square","graph","presentation","stats"],char:'💹',fitzpatrick_scale:false,category:"symbols"},sparkle:{keywords:["stars","green-square","awesome","good","fireworks"],char:'❇️',fitzpatrick_scale:false,category:"symbols"},eight_spoked_asterisk:{keywords:["star","sparkle","green-square"],char:'✳️',fitzpatrick_scale:false,category:"symbols"},negative_squared_cross_mark:{keywords:["x","green-square","no","deny"],char:'❎',fitzpatrick_scale:false,category:"symbols"},white_check_mark:{keywords:["green-square","ok","agree","vote","election","answer","tick"],char:'✅',fitzpatrick_scale:false,category:"symbols"},diamond_shape_with_a_dot_inside:{keywords:["jewel","blue","gem","crystal","fancy"],char:'💠',fitzpatrick_scale:false,category:"symbols"},cyclone:{keywords:["weather","swirl","blue","cloud","vortex","spiral","whirlpool","spin","tornado","hurricane","typhoon"],char:'🌀',fitzpatrick_scale:false,category:"symbols"},loop:{keywords:["tape","cassette"],char:'➿',fitzpatrick_scale:false,category:"symbols"},globe_with_meridians:{keywords:["earth","international","world","internet","interweb","i18n"],char:'🌐',fitzpatrick_scale:false,category:"symbols"},m:{keywords:["alphabet","blue-circle","letter"],char:'Ⓜ️',fitzpatrick_scale:false,category:"symbols"},atm:{keywords:["money","sales","cash","blue-square","payment","bank"],char:'🏧',fitzpatrick_scale:false,category:"symbols"},sa:{keywords:["japanese","blue-square","katakana"],char:'🈂️',fitzpatrick_scale:false,category:"symbols"},passport_control:{keywords:["custom","blue-square"],char:'🛂',fitzpatrick_scale:false,category:"symbols"},customs:{keywords:["passport","border","blue-square"],char:'🛃',fitzpatrick_scale:false,category:"symbols"},baggage_claim:{keywords:["blue-square","airport","transport"],char:'🛄',fitzpatrick_scale:false,category:"symbols"},left_luggage:{keywords:["blue-square","travel"],char:'🛅',fitzpatrick_scale:false,category:"symbols"},wheelchair:{keywords:["blue-square","disabled","a11y","accessibility"],char:'♿',fitzpatrick_scale:false,category:"symbols"},no_smoking:{keywords:["cigarette","blue-square","smell","smoke"],char:'🚭',fitzpatrick_scale:false,category:"symbols"},wc:{keywords:["toilet","restroom","blue-square"],char:'🚾',fitzpatrick_scale:false,category:"symbols"},parking:{keywords:["cars","blue-square","alphabet","letter"],char:'🅿️',fitzpatrick_scale:false,category:"symbols"},potable_water:{keywords:["blue-square","liquid","restroom","cleaning","faucet"],char:'🚰',fitzpatrick_scale:false,category:"symbols"},mens:{keywords:["toilet","restroom","wc","blue-square","gender","male"],char:'🚹',fitzpatrick_scale:false,category:"symbols"},womens:{keywords:["purple-square","woman","female","toilet","loo","restroom","gender"],char:'🚺',fitzpatrick_scale:false,category:"symbols"},baby_symbol:{keywords:["orange-square","child"],char:'🚼',fitzpatrick_scale:false,category:"symbols"},restroom:{keywords:["blue-square","toilet","refresh","wc","gender"],char:'🚻',fitzpatrick_scale:false,category:"symbols"},put_litter_in_its_place:{keywords:["blue-square","sign","human","info"],char:'🚮',fitzpatrick_scale:false,category:"symbols"},cinema:{keywords:["blue-square","record","film","movie","curtain","stage","theater"],char:'🎦',fitzpatrick_scale:false,category:"symbols"},signal_strength:{keywords:["blue-square","reception","phone","internet","connection","wifi","bluetooth","bars"],char:'📶',fitzpatrick_scale:false,category:"symbols"},koko:{keywords:["blue-square","here","katakana","japanese","destination"],char:'🈁',fitzpatrick_scale:false,category:"symbols"},ng:{keywords:["blue-square","words","shape","icon"],char:'🆖',fitzpatrick_scale:false,category:"symbols"},ok:{keywords:["good","agree","yes","blue-square"],char:'🆗',fitzpatrick_scale:false,category:"symbols"},up:{keywords:["blue-square","above","high"],char:'🆙',fitzpatrick_scale:false,category:"symbols"},cool:{keywords:["words","blue-square"],char:'🆒',fitzpatrick_scale:false,category:"symbols"},new:{keywords:["blue-square","words","start"],char:'🆕',fitzpatrick_scale:false,category:"symbols"},free:{keywords:["blue-square","words"],char:'🆓',fitzpatrick_scale:false,category:"symbols"},zero:{keywords:["0","numbers","blue-square","null"],char:'0️⃣',fitzpatrick_scale:false,category:"symbols"},one:{keywords:["blue-square","numbers","1"],char:'1️⃣',fitzpatrick_scale:false,category:"symbols"},two:{keywords:["numbers","2","prime","blue-square"],char:'2️⃣',fitzpatrick_scale:false,category:"symbols"},three:{keywords:["3","numbers","prime","blue-square"],char:'3️⃣',fitzpatrick_scale:false,category:"symbols"},four:{keywords:["4","numbers","blue-square"],char:'4️⃣',fitzpatrick_scale:false,category:"symbols"},five:{keywords:["5","numbers","blue-square","prime"],char:'5️⃣',fitzpatrick_scale:false,category:"symbols"},six:{keywords:["6","numbers","blue-square"],char:'6️⃣',fitzpatrick_scale:false,category:"symbols"},seven:{keywords:["7","numbers","blue-square","prime"],char:'7️⃣',fitzpatrick_scale:false,category:"symbols"},eight:{keywords:["8","blue-square","numbers"],char:'8️⃣',fitzpatrick_scale:false,category:"symbols"},nine:{keywords:["blue-square","numbers","9"],char:'9️⃣',fitzpatrick_scale:false,category:"symbols"},keycap_ten:{keywords:["numbers","10","blue-square"],char:'🔟',fitzpatrick_scale:false,category:"symbols"},asterisk:{keywords:["star","keycap"],char:'*⃣',fitzpatrick_scale:false,category:"symbols"},eject_button:{keywords:["blue-square"],char:'⏏️',fitzpatrick_scale:false,category:"symbols"},arrow_forward:{keywords:["blue-square","right","direction","play"],char:'▶️',fitzpatrick_scale:false,category:"symbols"},pause_button:{keywords:["pause","blue-square"],char:'⏸',fitzpatrick_scale:false,category:"symbols"},next_track_button:{keywords:["forward","next","blue-square"],char:'⏭',fitzpatrick_scale:false,category:"symbols"},stop_button:{keywords:["blue-square"],char:'⏹',fitzpatrick_scale:false,category:"symbols"},record_button:{keywords:["blue-square"],char:'⏺',fitzpatrick_scale:false,category:"symbols"},play_or_pause_button:{keywords:["blue-square","play","pause"],char:'⏯',fitzpatrick_scale:false,category:"symbols"},previous_track_button:{keywords:["backward"],char:'⏮',fitzpatrick_scale:false,category:"symbols"},fast_forward:{keywords:["blue-square","play","speed","continue"],char:'⏩',fitzpatrick_scale:false,category:"symbols"},rewind:{keywords:["play","blue-square"],char:'⏪',fitzpatrick_scale:false,category:"symbols"},twisted_rightwards_arrows:{keywords:["blue-square","shuffle","music","random"],char:'🔀',fitzpatrick_scale:false,category:"symbols"},repeat:{keywords:["loop","record"],char:'🔁',fitzpatrick_scale:false,category:"symbols"},repeat_one:{keywords:["blue-square","loop"],char:'🔂',fitzpatrick_scale:false,category:"symbols"},arrow_backward:{keywords:["blue-square","left","direction"],char:'◀️',fitzpatrick_scale:false,category:"symbols"},arrow_up_small:{keywords:["blue-square","triangle","direction","point","forward","top"],char:'🔼',fitzpatrick_scale:false,category:"symbols"},arrow_down_small:{keywords:["blue-square","direction","bottom"],char:'🔽',fitzpatrick_scale:false,category:"symbols"},arrow_double_up:{keywords:["blue-square","direction","top"],char:'⏫',fitzpatrick_scale:false,category:"symbols"},arrow_double_down:{keywords:["blue-square","direction","bottom"],char:'⏬',fitzpatrick_scale:false,category:"symbols"},arrow_right:{keywords:["blue-square","next"],char:'➡️',fitzpatrick_scale:false,category:"symbols"},arrow_left:{keywords:["blue-square","previous","back"],char:'⬅️',fitzpatrick_scale:false,category:"symbols"},arrow_up:{keywords:["blue-square","continue","top","direction"],char:'⬆️',fitzpatrick_scale:false,category:"symbols"},arrow_down:{keywords:["blue-square","direction","bottom"],char:'⬇️',fitzpatrick_scale:false,category:"symbols"},arrow_upper_right:{keywords:["blue-square","point","direction","diagonal","northeast"],char:'↗️',fitzpatrick_scale:false,category:"symbols"},arrow_lower_right:{keywords:["blue-square","direction","diagonal","southeast"],char:'↘️',fitzpatrick_scale:false,category:"symbols"},arrow_lower_left:{keywords:["blue-square","direction","diagonal","southwest"],char:'↙️',fitzpatrick_scale:false,category:"symbols"},arrow_upper_left:{keywords:["blue-square","point","direction","diagonal","northwest"],char:'↖️',fitzpatrick_scale:false,category:"symbols"},arrow_up_down:{keywords:["blue-square","direction","way","vertical"],char:'↕️',fitzpatrick_scale:false,category:"symbols"},left_right_arrow:{keywords:["shape","direction","horizontal","sideways"],char:'↔️',fitzpatrick_scale:false,category:"symbols"},arrows_counterclockwise:{keywords:["blue-square","sync","cycle"],char:'🔄',fitzpatrick_scale:false,category:"symbols"},arrow_right_hook:{keywords:["blue-square","return","rotate","direction"],char:'↪️',fitzpatrick_scale:false,category:"symbols"},leftwards_arrow_with_hook:{keywords:["back","return","blue-square","undo","enter"],char:'↩️',fitzpatrick_scale:false,category:"symbols"},arrow_heading_up:{keywords:["blue-square","direction","top"],char:'⤴️',fitzpatrick_scale:false,category:"symbols"},arrow_heading_down:{keywords:["blue-square","direction","bottom"],char:'⤵️',fitzpatrick_scale:false,category:"symbols"},hash:{keywords:["symbol","blue-square","twitter"],char:'#️⃣',fitzpatrick_scale:false,category:"symbols"},information_source:{keywords:["blue-square","alphabet","letter"],char:'ℹ️',fitzpatrick_scale:false,category:"symbols"},abc:{keywords:["blue-square","alphabet"],char:'🔤',fitzpatrick_scale:false,category:"symbols"},abcd:{keywords:["blue-square","alphabet"],char:'🔡',fitzpatrick_scale:false,category:"symbols"},capital_abcd:{keywords:["alphabet","words","blue-square"],char:'🔠',fitzpatrick_scale:false,category:"symbols"},symbols:{keywords:["blue-square","music","note","ampersand","percent","glyphs","characters"],char:'🔣',fitzpatrick_scale:false,category:"symbols"},musical_note:{keywords:["score","tone","sound"],char:'🎵',fitzpatrick_scale:false,category:"symbols"},notes:{keywords:["music","score"],char:'🎶',fitzpatrick_scale:false,category:"symbols"},wavy_dash:{keywords:["draw","line","moustache","mustache","squiggle","scribble"],char:'〰️',fitzpatrick_scale:false,category:"symbols"},curly_loop:{keywords:["scribble","draw","shape","squiggle"],char:'➰',fitzpatrick_scale:false,category:"symbols"},heavy_check_mark:{keywords:["ok","nike","answer","yes","tick"],char:'✔️',fitzpatrick_scale:false,category:"symbols"},arrows_clockwise:{keywords:["sync","cycle","round","repeat"],char:'🔃',fitzpatrick_scale:false,category:"symbols"},heavy_plus_sign:{keywords:["math","calculation","addition","more","increase"],char:'➕',fitzpatrick_scale:false,category:"symbols"},heavy_minus_sign:{keywords:["math","calculation","subtract","less"],char:'➖',fitzpatrick_scale:false,category:"symbols"},heavy_division_sign:{keywords:["divide","math","calculation"],char:'➗',fitzpatrick_scale:false,category:"symbols"},heavy_multiplication_x:{keywords:["math","calculation"],char:'✖️',fitzpatrick_scale:false,category:"symbols"},infinity:{keywords:["forever"],char:'♾',fitzpatrick_scale:false,category:"symbols"},heavy_dollar_sign:{keywords:["money","sales","payment","currency","buck"],char:'💲',fitzpatrick_scale:false,category:"symbols"},currency_exchange:{keywords:["money","sales","dollar","travel"],char:'💱',fitzpatrick_scale:false,category:"symbols"},copyright:{keywords:["ip","license","circle","law","legal"],char:'©️',fitzpatrick_scale:false,category:"symbols"},registered:{keywords:["alphabet","circle"],char:'®️',fitzpatrick_scale:false,category:"symbols"},tm:{keywords:["trademark","brand","law","legal"],char:'™️',fitzpatrick_scale:false,category:"symbols"},end:{keywords:["words","arrow"],char:'🔚',fitzpatrick_scale:false,category:"symbols"},back:{keywords:["arrow","words","return"],char:'🔙',fitzpatrick_scale:false,category:"symbols"},on:{keywords:["arrow","words"],char:'🔛',fitzpatrick_scale:false,category:"symbols"},top:{keywords:["words","blue-square"],char:'🔝',fitzpatrick_scale:false,category:"symbols"},soon:{keywords:["arrow","words"],char:'🔜',fitzpatrick_scale:false,category:"symbols"},ballot_box_with_check:{keywords:["ok","agree","confirm","black-square","vote","election","yes","tick"],char:'☑️',fitzpatrick_scale:false,category:"symbols"},radio_button:{keywords:["input","old","music","circle"],char:'🔘',fitzpatrick_scale:false,category:"symbols"},white_circle:{keywords:["shape","round"],char:'⚪',fitzpatrick_scale:false,category:"symbols"},black_circle:{keywords:["shape","button","round"],char:'⚫',fitzpatrick_scale:false,category:"symbols"},red_circle:{keywords:["shape","error","danger"],char:'🔴',fitzpatrick_scale:false,category:"symbols"},large_blue_circle:{keywords:["shape","icon","button"],char:'🔵',fitzpatrick_scale:false,category:"symbols"},small_orange_diamond:{keywords:["shape","jewel","gem"],char:'🔸',fitzpatrick_scale:false,category:"symbols"},small_blue_diamond:{keywords:["shape","jewel","gem"],char:'🔹',fitzpatrick_scale:false,category:"symbols"},large_orange_diamond:{keywords:["shape","jewel","gem"],char:'🔶',fitzpatrick_scale:false,category:"symbols"},large_blue_diamond:{keywords:["shape","jewel","gem"],char:'🔷',fitzpatrick_scale:false,category:"symbols"},small_red_triangle:{keywords:["shape","direction","up","top"],char:'🔺',fitzpatrick_scale:false,category:"symbols"},black_small_square:{keywords:["shape","icon"],char:'▪️',fitzpatrick_scale:false,category:"symbols"},white_small_square:{keywords:["shape","icon"],char:'▫️',fitzpatrick_scale:false,category:"symbols"},black_large_square:{keywords:["shape","icon","button"],char:'⬛',fitzpatrick_scale:false,category:"symbols"},white_large_square:{keywords:["shape","icon","stone","button"],char:'⬜',fitzpatrick_scale:false,category:"symbols"},small_red_triangle_down:{keywords:["shape","direction","bottom"],char:'🔻',fitzpatrick_scale:false,category:"symbols"},black_medium_square:{keywords:["shape","button","icon"],char:'◼️',fitzpatrick_scale:false,category:"symbols"},white_medium_square:{keywords:["shape","stone","icon"],char:'◻️',fitzpatrick_scale:false,category:"symbols"},black_medium_small_square:{keywords:["icon","shape","button"],char:'◾',fitzpatrick_scale:false,category:"symbols"},white_medium_small_square:{keywords:["shape","stone","icon","button"],char:'◽',fitzpatrick_scale:false,category:"symbols"},black_square_button:{keywords:["shape","input","frame"],char:'🔲',fitzpatrick_scale:false,category:"symbols"},white_square_button:{keywords:["shape","input"],char:'🔳',fitzpatrick_scale:false,category:"symbols"},speaker:{keywords:["sound","volume","silence","broadcast"],char:'🔈',fitzpatrick_scale:false,category:"symbols"},sound:{keywords:["volume","speaker","broadcast"],char:'🔉',fitzpatrick_scale:false,category:"symbols"},loud_sound:{keywords:["volume","noise","noisy","speaker","broadcast"],char:'🔊',fitzpatrick_scale:false,category:"symbols"},mute:{keywords:["sound","volume","silence","quiet"],char:'🔇',fitzpatrick_scale:false,category:"symbols"},mega:{keywords:["sound","speaker","volume"],char:'📣',fitzpatrick_scale:false,category:"symbols"},loudspeaker:{keywords:["volume","sound"],char:'📢',fitzpatrick_scale:false,category:"symbols"},bell:{keywords:["sound","notification","christmas","xmas","chime"],char:'🔔',fitzpatrick_scale:false,category:"symbols"},no_bell:{keywords:["sound","volume","mute","quiet","silent"],char:'🔕',fitzpatrick_scale:false,category:"symbols"},black_joker:{keywords:["poker","cards","game","play","magic"],char:'🃏',fitzpatrick_scale:false,category:"symbols"},mahjong:{keywords:["game","play","chinese","kanji"],char:'🀄',fitzpatrick_scale:false,category:"symbols"},spades:{keywords:["poker","cards","suits","magic"],char:'♠️',fitzpatrick_scale:false,category:"symbols"},clubs:{keywords:["poker","cards","magic","suits"],char:'♣️',fitzpatrick_scale:false,category:"symbols"},hearts:{keywords:["poker","cards","magic","suits"],char:'♥️',fitzpatrick_scale:false,category:"symbols"},diamonds:{keywords:["poker","cards","magic","suits"],char:'♦️',fitzpatrick_scale:false,category:"symbols"},flower_playing_cards:{keywords:["game","sunset","red"],char:'🎴',fitzpatrick_scale:false,category:"symbols"},thought_balloon:{keywords:["bubble","cloud","speech","thinking","dream"],char:'💭',fitzpatrick_scale:false,category:"symbols"},right_anger_bubble:{keywords:["caption","speech","thinking","mad"],char:'🗯',fitzpatrick_scale:false,category:"symbols"},speech_balloon:{keywords:["bubble","words","message","talk","chatting"],char:'💬',fitzpatrick_scale:false,category:"symbols"},left_speech_bubble:{keywords:["words","message","talk","chatting"],char:'🗨',fitzpatrick_scale:false,category:"symbols"},clock1:{keywords:["time","late","early","schedule"],char:'🕐',fitzpatrick_scale:false,category:"symbols"},clock2:{keywords:["time","late","early","schedule"],char:'🕑',fitzpatrick_scale:false,category:"symbols"},clock3:{keywords:["time","late","early","schedule"],char:'🕒',fitzpatrick_scale:false,category:"symbols"},clock4:{keywords:["time","late","early","schedule"],char:'🕓',fitzpatrick_scale:false,category:"symbols"},clock5:{keywords:["time","late","early","schedule"],char:'🕔',fitzpatrick_scale:false,category:"symbols"},clock6:{keywords:["time","late","early","schedule","dawn","dusk"],char:'🕕',fitzpatrick_scale:false,category:"symbols"},clock7:{keywords:["time","late","early","schedule"],char:'🕖',fitzpatrick_scale:false,category:"symbols"},clock8:{keywords:["time","late","early","schedule"],char:'🕗',fitzpatrick_scale:false,category:"symbols"},clock9:{keywords:["time","late","early","schedule"],char:'🕘',fitzpatrick_scale:false,category:"symbols"},clock10:{keywords:["time","late","early","schedule"],char:'🕙',fitzpatrick_scale:false,category:"symbols"},clock11:{keywords:["time","late","early","schedule"],char:'🕚',fitzpatrick_scale:false,category:"symbols"},clock12:{keywords:["time","noon","midnight","midday","late","early","schedule"],char:'🕛',fitzpatrick_scale:false,category:"symbols"},clock130:{keywords:["time","late","early","schedule"],char:'🕜',fitzpatrick_scale:false,category:"symbols"},clock230:{keywords:["time","late","early","schedule"],char:'🕝',fitzpatrick_scale:false,category:"symbols"},clock330:{keywords:["time","late","early","schedule"],char:'🕞',fitzpatrick_scale:false,category:"symbols"},clock430:{keywords:["time","late","early","schedule"],char:'🕟',fitzpatrick_scale:false,category:"symbols"},clock530:{keywords:["time","late","early","schedule"],char:'🕠',fitzpatrick_scale:false,category:"symbols"},clock630:{keywords:["time","late","early","schedule"],char:'🕡',fitzpatrick_scale:false,category:"symbols"},clock730:{keywords:["time","late","early","schedule"],char:'🕢',fitzpatrick_scale:false,category:"symbols"},clock830:{keywords:["time","late","early","schedule"],char:'🕣',fitzpatrick_scale:false,category:"symbols"},clock930:{keywords:["time","late","early","schedule"],char:'🕤',fitzpatrick_scale:false,category:"symbols"},clock1030:{keywords:["time","late","early","schedule"],char:'🕥',fitzpatrick_scale:false,category:"symbols"},clock1130:{keywords:["time","late","early","schedule"],char:'🕦',fitzpatrick_scale:false,category:"symbols"},clock1230:{keywords:["time","late","early","schedule"],char:'🕧',fitzpatrick_scale:false,category:"symbols"},afghanistan:{keywords:["af","flag","nation","country","banner"],char:'🇦🇫',fitzpatrick_scale:false,category:"flags"},aland_islands:{keywords:["Åland","islands","flag","nation","country","banner"],char:'🇦🇽',fitzpatrick_scale:false,category:"flags"},albania:{keywords:["al","flag","nation","country","banner"],char:'🇦🇱',fitzpatrick_scale:false,category:"flags"},algeria:{keywords:["dz","flag","nation","country","banner"],char:'🇩🇿',fitzpatrick_scale:false,category:"flags"},american_samoa:{keywords:["american","ws","flag","nation","country","banner"],char:'🇦🇸',fitzpatrick_scale:false,category:"flags"},andorra:{keywords:["ad","flag","nation","country","banner"],char:'🇦🇩',fitzpatrick_scale:false,category:"flags"},angola:{keywords:["ao","flag","nation","country","banner"],char:'🇦🇴',fitzpatrick_scale:false,category:"flags"},anguilla:{keywords:["ai","flag","nation","country","banner"],char:'🇦🇮',fitzpatrick_scale:false,category:"flags"},antarctica:{keywords:["aq","flag","nation","country","banner"],char:'🇦🇶',fitzpatrick_scale:false,category:"flags"},antigua_barbuda:{keywords:["antigua","barbuda","flag","nation","country","banner"],char:'🇦🇬',fitzpatrick_scale:false,category:"flags"},argentina:{keywords:["ar","flag","nation","country","banner"],char:'🇦🇷',fitzpatrick_scale:false,category:"flags"},armenia:{keywords:["am","flag","nation","country","banner"],char:'🇦🇲',fitzpatrick_scale:false,category:"flags"},aruba:{keywords:["aw","flag","nation","country","banner"],char:'🇦🇼',fitzpatrick_scale:false,category:"flags"},australia:{keywords:["au","flag","nation","country","banner"],char:'🇦🇺',fitzpatrick_scale:false,category:"flags"},austria:{keywords:["at","flag","nation","country","banner"],char:'🇦🇹',fitzpatrick_scale:false,category:"flags"},azerbaijan:{keywords:["az","flag","nation","country","banner"],char:'🇦🇿',fitzpatrick_scale:false,category:"flags"},bahamas:{keywords:["bs","flag","nation","country","banner"],char:'🇧🇸',fitzpatrick_scale:false,category:"flags"},bahrain:{keywords:["bh","flag","nation","country","banner"],char:'🇧🇭',fitzpatrick_scale:false,category:"flags"},bangladesh:{keywords:["bd","flag","nation","country","banner"],char:'🇧🇩',fitzpatrick_scale:false,category:"flags"},barbados:{keywords:["bb","flag","nation","country","banner"],char:'🇧🇧',fitzpatrick_scale:false,category:"flags"},belarus:{keywords:["by","flag","nation","country","banner"],char:'🇧🇾',fitzpatrick_scale:false,category:"flags"},belgium:{keywords:["be","flag","nation","country","banner"],char:'🇧🇪',fitzpatrick_scale:false,category:"flags"},belize:{keywords:["bz","flag","nation","country","banner"],char:'🇧🇿',fitzpatrick_scale:false,category:"flags"},benin:{keywords:["bj","flag","nation","country","banner"],char:'🇧🇯',fitzpatrick_scale:false,category:"flags"},bermuda:{keywords:["bm","flag","nation","country","banner"],char:'🇧🇲',fitzpatrick_scale:false,category:"flags"},bhutan:{keywords:["bt","flag","nation","country","banner"],char:'🇧🇹',fitzpatrick_scale:false,category:"flags"},bolivia:{keywords:["bo","flag","nation","country","banner"],char:'🇧🇴',fitzpatrick_scale:false,category:"flags"},caribbean_netherlands:{keywords:["bonaire","flag","nation","country","banner"],char:'🇧🇶',fitzpatrick_scale:false,category:"flags"},bosnia_herzegovina:{keywords:["bosnia","herzegovina","flag","nation","country","banner"],char:'🇧🇦',fitzpatrick_scale:false,category:"flags"},botswana:{keywords:["bw","flag","nation","country","banner"],char:'🇧🇼',fitzpatrick_scale:false,category:"flags"},brazil:{keywords:["br","flag","nation","country","banner"],char:'🇧🇷',fitzpatrick_scale:false,category:"flags"},british_indian_ocean_territory:{keywords:["british","indian","ocean","territory","flag","nation","country","banner"],char:'🇮🇴',fitzpatrick_scale:false,category:"flags"},british_virgin_islands:{keywords:["british","virgin","islands","bvi","flag","nation","country","banner"],char:'🇻🇬',fitzpatrick_scale:false,category:"flags"},brunei:{keywords:["bn","darussalam","flag","nation","country","banner"],char:'🇧🇳',fitzpatrick_scale:false,category:"flags"},bulgaria:{keywords:["bg","flag","nation","country","banner"],char:'🇧🇬',fitzpatrick_scale:false,category:"flags"},burkina_faso:{keywords:["burkina","faso","flag","nation","country","banner"],char:'🇧🇫',fitzpatrick_scale:false,category:"flags"},burundi:{keywords:["bi","flag","nation","country","banner"],char:'🇧🇮',fitzpatrick_scale:false,category:"flags"},cape_verde:{keywords:["cabo","verde","flag","nation","country","banner"],char:'🇨🇻',fitzpatrick_scale:false,category:"flags"},cambodia:{keywords:["kh","flag","nation","country","banner"],char:'🇰🇭',fitzpatrick_scale:false,category:"flags"},cameroon:{keywords:["cm","flag","nation","country","banner"],char:'🇨🇲',fitzpatrick_scale:false,category:"flags"},canada:{keywords:["ca","flag","nation","country","banner"],char:'🇨🇦',fitzpatrick_scale:false,category:"flags"},canary_islands:{keywords:["canary","islands","flag","nation","country","banner"],char:'🇮🇨',fitzpatrick_scale:false,category:"flags"},cayman_islands:{keywords:["cayman","islands","flag","nation","country","banner"],char:'🇰🇾',fitzpatrick_scale:false,category:"flags"},central_african_republic:{keywords:["central","african","republic","flag","nation","country","banner"],char:'🇨🇫',fitzpatrick_scale:false,category:"flags"},chad:{keywords:["td","flag","nation","country","banner"],char:'🇹🇩',fitzpatrick_scale:false,category:"flags"},chile:{keywords:["flag","nation","country","banner"],char:'🇨🇱',fitzpatrick_scale:false,category:"flags"},cn:{keywords:["china","chinese","prc","flag","country","nation","banner"],char:'🇨🇳',fitzpatrick_scale:false,category:"flags"},christmas_island:{keywords:["christmas","island","flag","nation","country","banner"],char:'🇨🇽',fitzpatrick_scale:false,category:"flags"},cocos_islands:{keywords:["cocos","keeling","islands","flag","nation","country","banner"],char:'🇨🇨',fitzpatrick_scale:false,category:"flags"},colombia:{keywords:["co","flag","nation","country","banner"],char:'🇨🇴',fitzpatrick_scale:false,category:"flags"},comoros:{keywords:["km","flag","nation","country","banner"],char:'🇰🇲',fitzpatrick_scale:false,category:"flags"},congo_brazzaville:{keywords:["congo","flag","nation","country","banner"],char:'🇨🇬',fitzpatrick_scale:false,category:"flags"},congo_kinshasa:{keywords:["congo","democratic","republic","flag","nation","country","banner"],char:'🇨🇩',fitzpatrick_scale:false,category:"flags"},cook_islands:{keywords:["cook","islands","flag","nation","country","banner"],char:'🇨🇰',fitzpatrick_scale:false,category:"flags"},costa_rica:{keywords:["costa","rica","flag","nation","country","banner"],char:'🇨🇷',fitzpatrick_scale:false,category:"flags"},croatia:{keywords:["hr","flag","nation","country","banner"],char:'🇭🇷',fitzpatrick_scale:false,category:"flags"},cuba:{keywords:["cu","flag","nation","country","banner"],char:'🇨🇺',fitzpatrick_scale:false,category:"flags"},curacao:{keywords:["curaçao","flag","nation","country","banner"],char:'🇨🇼',fitzpatrick_scale:false,category:"flags"},cyprus:{keywords:["cy","flag","nation","country","banner"],char:'🇨🇾',fitzpatrick_scale:false,category:"flags"},czech_republic:{keywords:["cz","flag","nation","country","banner"],char:'🇨🇿',fitzpatrick_scale:false,category:"flags"},denmark:{keywords:["dk","flag","nation","country","banner"],char:'🇩🇰',fitzpatrick_scale:false,category:"flags"},djibouti:{keywords:["dj","flag","nation","country","banner"],char:'🇩🇯',fitzpatrick_scale:false,category:"flags"},dominica:{keywords:["dm","flag","nation","country","banner"],char:'🇩🇲',fitzpatrick_scale:false,category:"flags"},dominican_republic:{keywords:["dominican","republic","flag","nation","country","banner"],char:'🇩🇴',fitzpatrick_scale:false,category:"flags"},ecuador:{keywords:["ec","flag","nation","country","banner"],char:'🇪🇨',fitzpatrick_scale:false,category:"flags"},egypt:{keywords:["eg","flag","nation","country","banner"],char:'🇪🇬',fitzpatrick_scale:false,category:"flags"},el_salvador:{keywords:["el","salvador","flag","nation","country","banner"],char:'🇸🇻',fitzpatrick_scale:false,category:"flags"},equatorial_guinea:{keywords:["equatorial","gn","flag","nation","country","banner"],char:'🇬🇶',fitzpatrick_scale:false,category:"flags"},eritrea:{keywords:["er","flag","nation","country","banner"],char:'🇪🇷',fitzpatrick_scale:false,category:"flags"},estonia:{keywords:["ee","flag","nation","country","banner"],char:'🇪🇪',fitzpatrick_scale:false,category:"flags"},ethiopia:{keywords:["et","flag","nation","country","banner"],char:'🇪🇹',fitzpatrick_scale:false,category:"flags"},eu:{keywords:["european","union","flag","banner"],char:'🇪🇺',fitzpatrick_scale:false,category:"flags"},falkland_islands:{keywords:["falkland","islands","malvinas","flag","nation","country","banner"],char:'🇫🇰',fitzpatrick_scale:false,category:"flags"},faroe_islands:{keywords:["faroe","islands","flag","nation","country","banner"],char:'🇫🇴',fitzpatrick_scale:false,category:"flags"},fiji:{keywords:["fj","flag","nation","country","banner"],char:'🇫🇯',fitzpatrick_scale:false,category:"flags"},finland:{keywords:["fi","flag","nation","country","banner"],char:'🇫🇮',fitzpatrick_scale:false,category:"flags"},fr:{keywords:["banner","flag","nation","france","french","country"],char:'🇫🇷',fitzpatrick_scale:false,category:"flags"},french_guiana:{keywords:["french","guiana","flag","nation","country","banner"],char:'🇬🇫',fitzpatrick_scale:false,category:"flags"},french_polynesia:{keywords:["french","polynesia","flag","nation","country","banner"],char:'🇵🇫',fitzpatrick_scale:false,category:"flags"},french_southern_territories:{keywords:["french","southern","territories","flag","nation","country","banner"],char:'🇹🇫',fitzpatrick_scale:false,category:"flags"},gabon:{keywords:["ga","flag","nation","country","banner"],char:'🇬🇦',fitzpatrick_scale:false,category:"flags"},gambia:{keywords:["gm","flag","nation","country","banner"],char:'🇬🇲',fitzpatrick_scale:false,category:"flags"},georgia:{keywords:["ge","flag","nation","country","banner"],char:'🇬🇪',fitzpatrick_scale:false,category:"flags"},de:{keywords:["german","nation","flag","country","banner"],char:'🇩🇪',fitzpatrick_scale:false,category:"flags"},ghana:{keywords:["gh","flag","nation","country","banner"],char:'🇬🇭',fitzpatrick_scale:false,category:"flags"},gibraltar:{keywords:["gi","flag","nation","country","banner"],char:'🇬🇮',fitzpatrick_scale:false,category:"flags"},greece:{keywords:["gr","flag","nation","country","banner"],char:'🇬🇷',fitzpatrick_scale:false,category:"flags"},greenland:{keywords:["gl","flag","nation","country","banner"],char:'🇬🇱',fitzpatrick_scale:false,category:"flags"},grenada:{keywords:["gd","flag","nation","country","banner"],char:'🇬🇩',fitzpatrick_scale:false,category:"flags"},guadeloupe:{keywords:["gp","flag","nation","country","banner"],char:'🇬🇵',fitzpatrick_scale:false,category:"flags"},guam:{keywords:["gu","flag","nation","country","banner"],char:'🇬🇺',fitzpatrick_scale:false,category:"flags"},guatemala:{keywords:["gt","flag","nation","country","banner"],char:'🇬🇹',fitzpatrick_scale:false,category:"flags"},guernsey:{keywords:["gg","flag","nation","country","banner"],char:'🇬🇬',fitzpatrick_scale:false,category:"flags"},guinea:{keywords:["gn","flag","nation","country","banner"],char:'🇬🇳',fitzpatrick_scale:false,category:"flags"},guinea_bissau:{keywords:["gw","bissau","flag","nation","country","banner"],char:'🇬🇼',fitzpatrick_scale:false,category:"flags"},guyana:{keywords:["gy","flag","nation","country","banner"],char:'🇬🇾',fitzpatrick_scale:false,category:"flags"},haiti:{keywords:["ht","flag","nation","country","banner"],char:'🇭🇹',fitzpatrick_scale:false,category:"flags"},honduras:{keywords:["hn","flag","nation","country","banner"],char:'🇭🇳',fitzpatrick_scale:false,category:"flags"},hong_kong:{keywords:["hong","kong","flag","nation","country","banner"],char:'🇭🇰',fitzpatrick_scale:false,category:"flags"},hungary:{keywords:["hu","flag","nation","country","banner"],char:'🇭🇺',fitzpatrick_scale:false,category:"flags"},iceland:{keywords:["is","flag","nation","country","banner"],char:'🇮🇸',fitzpatrick_scale:false,category:"flags"},india:{keywords:["in","flag","nation","country","banner"],char:'🇮🇳',fitzpatrick_scale:false,category:"flags"},indonesia:{keywords:["flag","nation","country","banner"],char:'🇮🇩',fitzpatrick_scale:false,category:"flags"},iran:{keywords:["iran,","islamic","republic","flag","nation","country","banner"],char:'🇮🇷',fitzpatrick_scale:false,category:"flags"},iraq:{keywords:["iq","flag","nation","country","banner"],char:'🇮🇶',fitzpatrick_scale:false,category:"flags"},ireland:{keywords:["ie","flag","nation","country","banner"],char:'🇮🇪',fitzpatrick_scale:false,category:"flags"},isle_of_man:{keywords:["isle","man","flag","nation","country","banner"],char:'🇮🇲',fitzpatrick_scale:false,category:"flags"},israel:{keywords:["il","flag","nation","country","banner"],char:'🇮🇱',fitzpatrick_scale:false,category:"flags"},it:{keywords:["italy","flag","nation","country","banner"],char:'🇮🇹',fitzpatrick_scale:false,category:"flags"},cote_divoire:{keywords:["ivory","coast","flag","nation","country","banner"],char:'🇨🇮',fitzpatrick_scale:false,category:"flags"},jamaica:{keywords:["jm","flag","nation","country","banner"],char:'🇯🇲',fitzpatrick_scale:false,category:"flags"},jp:{keywords:["japanese","nation","flag","country","banner"],char:'🇯🇵',fitzpatrick_scale:false,category:"flags"},jersey:{keywords:["je","flag","nation","country","banner"],char:'🇯🇪',fitzpatrick_scale:false,category:"flags"},jordan:{keywords:["jo","flag","nation","country","banner"],char:'🇯🇴',fitzpatrick_scale:false,category:"flags"},kazakhstan:{keywords:["kz","flag","nation","country","banner"],char:'🇰🇿',fitzpatrick_scale:false,category:"flags"},kenya:{keywords:["ke","flag","nation","country","banner"],char:'🇰🇪',fitzpatrick_scale:false,category:"flags"},kiribati:{keywords:["ki","flag","nation","country","banner"],char:'🇰🇮',fitzpatrick_scale:false,category:"flags"},kosovo:{keywords:["xk","flag","nation","country","banner"],char:'🇽🇰',fitzpatrick_scale:false,category:"flags"},kuwait:{keywords:["kw","flag","nation","country","banner"],char:'🇰🇼',fitzpatrick_scale:false,category:"flags"},kyrgyzstan:{keywords:["kg","flag","nation","country","banner"],char:'🇰🇬',fitzpatrick_scale:false,category:"flags"},laos:{keywords:["lao","democratic","republic","flag","nation","country","banner"],char:'🇱🇦',fitzpatrick_scale:false,category:"flags"},latvia:{keywords:["lv","flag","nation","country","banner"],char:'🇱🇻',fitzpatrick_scale:false,category:"flags"},lebanon:{keywords:["lb","flag","nation","country","banner"],char:'🇱🇧',fitzpatrick_scale:false,category:"flags"},lesotho:{keywords:["ls","flag","nation","country","banner"],char:'🇱🇸',fitzpatrick_scale:false,category:"flags"},liberia:{keywords:["lr","flag","nation","country","banner"],char:'🇱🇷',fitzpatrick_scale:false,category:"flags"},libya:{keywords:["ly","flag","nation","country","banner"],char:'🇱🇾',fitzpatrick_scale:false,category:"flags"},liechtenstein:{keywords:["li","flag","nation","country","banner"],char:'🇱🇮',fitzpatrick_scale:false,category:"flags"},lithuania:{keywords:["lt","flag","nation","country","banner"],char:'🇱🇹',fitzpatrick_scale:false,category:"flags"},luxembourg:{keywords:["lu","flag","nation","country","banner"],char:'🇱🇺',fitzpatrick_scale:false,category:"flags"},macau:{keywords:["macao","flag","nation","country","banner"],char:'🇲🇴',fitzpatrick_scale:false,category:"flags"},macedonia:{keywords:["macedonia,","flag","nation","country","banner"],char:'🇲🇰',fitzpatrick_scale:false,category:"flags"},madagascar:{keywords:["mg","flag","nation","country","banner"],char:'🇲🇬',fitzpatrick_scale:false,category:"flags"},malawi:{keywords:["mw","flag","nation","country","banner"],char:'🇲🇼',fitzpatrick_scale:false,category:"flags"},malaysia:{keywords:["my","flag","nation","country","banner"],char:'🇲🇾',fitzpatrick_scale:false,category:"flags"},maldives:{keywords:["mv","flag","nation","country","banner"],char:'🇲🇻',fitzpatrick_scale:false,category:"flags"},mali:{keywords:["ml","flag","nation","country","banner"],char:'🇲🇱',fitzpatrick_scale:false,category:"flags"},malta:{keywords:["mt","flag","nation","country","banner"],char:'🇲🇹',fitzpatrick_scale:false,category:"flags"},marshall_islands:{keywords:["marshall","islands","flag","nation","country","banner"],char:'🇲🇭',fitzpatrick_scale:false,category:"flags"},martinique:{keywords:["mq","flag","nation","country","banner"],char:'🇲🇶',fitzpatrick_scale:false,category:"flags"},mauritania:{keywords:["mr","flag","nation","country","banner"],char:'🇲🇷',fitzpatrick_scale:false,category:"flags"},mauritius:{keywords:["mu","flag","nation","country","banner"],char:'🇲🇺',fitzpatrick_scale:false,category:"flags"},mayotte:{keywords:["yt","flag","nation","country","banner"],char:'🇾🇹',fitzpatrick_scale:false,category:"flags"},mexico:{keywords:["mx","flag","nation","country","banner"],char:'🇲🇽',fitzpatrick_scale:false,category:"flags"},micronesia:{keywords:["micronesia,","federated","states","flag","nation","country","banner"],char:'🇫🇲',fitzpatrick_scale:false,category:"flags"},moldova:{keywords:["moldova,","republic","flag","nation","country","banner"],char:'🇲🇩',fitzpatrick_scale:false,category:"flags"},monaco:{keywords:["mc","flag","nation","country","banner"],char:'🇲🇨',fitzpatrick_scale:false,category:"flags"},mongolia:{keywords:["mn","flag","nation","country","banner"],char:'🇲🇳',fitzpatrick_scale:false,category:"flags"},montenegro:{keywords:["me","flag","nation","country","banner"],char:'🇲🇪',fitzpatrick_scale:false,category:"flags"},montserrat:{keywords:["ms","flag","nation","country","banner"],char:'🇲🇸',fitzpatrick_scale:false,category:"flags"},morocco:{keywords:["ma","flag","nation","country","banner"],char:'🇲🇦',fitzpatrick_scale:false,category:"flags"},mozambique:{keywords:["mz","flag","nation","country","banner"],char:'🇲🇿',fitzpatrick_scale:false,category:"flags"},myanmar:{keywords:["mm","flag","nation","country","banner"],char:'🇲🇲',fitzpatrick_scale:false,category:"flags"},namibia:{keywords:["na","flag","nation","country","banner"],char:'🇳🇦',fitzpatrick_scale:false,category:"flags"},nauru:{keywords:["nr","flag","nation","country","banner"],char:'🇳🇷',fitzpatrick_scale:false,category:"flags"},nepal:{keywords:["np","flag","nation","country","banner"],char:'🇳🇵',fitzpatrick_scale:false,category:"flags"},netherlands:{keywords:["nl","flag","nation","country","banner"],char:'🇳🇱',fitzpatrick_scale:false,category:"flags"},new_caledonia:{keywords:["new","caledonia","flag","nation","country","banner"],char:'🇳🇨',fitzpatrick_scale:false,category:"flags"},new_zealand:{keywords:["new","zealand","flag","nation","country","banner"],char:'🇳🇿',fitzpatrick_scale:false,category:"flags"},nicaragua:{keywords:["ni","flag","nation","country","banner"],char:'🇳🇮',fitzpatrick_scale:false,category:"flags"},niger:{keywords:["ne","flag","nation","country","banner"],char:'🇳🇪',fitzpatrick_scale:false,category:"flags"},nigeria:{keywords:["flag","nation","country","banner"],char:'🇳🇬',fitzpatrick_scale:false,category:"flags"},niue:{keywords:["nu","flag","nation","country","banner"],char:'🇳🇺',fitzpatrick_scale:false,category:"flags"},norfolk_island:{keywords:["norfolk","island","flag","nation","country","banner"],char:'🇳🇫',fitzpatrick_scale:false,category:"flags"},northern_mariana_islands:{keywords:["northern","mariana","islands","flag","nation","country","banner"],char:'🇲🇵',fitzpatrick_scale:false,category:"flags"},north_korea:{keywords:["north","korea","nation","flag","country","banner"],char:'🇰🇵',fitzpatrick_scale:false,category:"flags"},norway:{keywords:["no","flag","nation","country","banner"],char:'🇳🇴',fitzpatrick_scale:false,category:"flags"},oman:{keywords:["om_symbol","flag","nation","country","banner"],char:'🇴🇲',fitzpatrick_scale:false,category:"flags"},pakistan:{keywords:["pk","flag","nation","country","banner"],char:'🇵🇰',fitzpatrick_scale:false,category:"flags"},palau:{keywords:["pw","flag","nation","country","banner"],char:'🇵🇼',fitzpatrick_scale:false,category:"flags"},palestinian_territories:{keywords:["palestine","palestinian","territories","flag","nation","country","banner"],char:'🇵🇸',fitzpatrick_scale:false,category:"flags"},panama:{keywords:["pa","flag","nation","country","banner"],char:'🇵🇦',fitzpatrick_scale:false,category:"flags"},papua_new_guinea:{keywords:["papua","new","guinea","flag","nation","country","banner"],char:'🇵🇬',fitzpatrick_scale:false,category:"flags"},paraguay:{keywords:["py","flag","nation","country","banner"],char:'🇵🇾',fitzpatrick_scale:false,category:"flags"},peru:{keywords:["pe","flag","nation","country","banner"],char:'🇵🇪',fitzpatrick_scale:false,category:"flags"},philippines:{keywords:["ph","flag","nation","country","banner"],char:'🇵🇭',fitzpatrick_scale:false,category:"flags"},pitcairn_islands:{keywords:["pitcairn","flag","nation","country","banner"],char:'🇵🇳',fitzpatrick_scale:false,category:"flags"},poland:{keywords:["pl","flag","nation","country","banner"],char:'🇵🇱',fitzpatrick_scale:false,category:"flags"},portugal:{keywords:["pt","flag","nation","country","banner"],char:'🇵🇹',fitzpatrick_scale:false,category:"flags"},puerto_rico:{keywords:["puerto","rico","flag","nation","country","banner"],char:'🇵🇷',fitzpatrick_scale:false,category:"flags"},qatar:{keywords:["qa","flag","nation","country","banner"],char:'🇶🇦',fitzpatrick_scale:false,category:"flags"},reunion:{keywords:["réunion","flag","nation","country","banner"],char:'🇷🇪',fitzpatrick_scale:false,category:"flags"},romania:{keywords:["ro","flag","nation","country","banner"],char:'🇷🇴',fitzpatrick_scale:false,category:"flags"},ru:{keywords:["russian","federation","flag","nation","country","banner"],char:'🇷🇺',fitzpatrick_scale:false,category:"flags"},rwanda:{keywords:["rw","flag","nation","country","banner"],char:'🇷🇼',fitzpatrick_scale:false,category:"flags"},st_barthelemy:{keywords:["saint","barthélemy","flag","nation","country","banner"],char:'🇧🇱',fitzpatrick_scale:false,category:"flags"},st_helena:{keywords:["saint","helena","ascension","tristan","cunha","flag","nation","country","banner"],char:'🇸🇭',fitzpatrick_scale:false,category:"flags"},st_kitts_nevis:{keywords:["saint","kitts","nevis","flag","nation","country","banner"],char:'🇰🇳',fitzpatrick_scale:false,category:"flags"},st_lucia:{keywords:["saint","lucia","flag","nation","country","banner"],char:'🇱🇨',fitzpatrick_scale:false,category:"flags"},st_pierre_miquelon:{keywords:["saint","pierre","miquelon","flag","nation","country","banner"],char:'🇵🇲',fitzpatrick_scale:false,category:"flags"},st_vincent_grenadines:{keywords:["saint","vincent","grenadines","flag","nation","country","banner"],char:'🇻🇨',fitzpatrick_scale:false,category:"flags"},samoa:{keywords:["ws","flag","nation","country","banner"],char:'🇼🇸',fitzpatrick_scale:false,category:"flags"},san_marino:{keywords:["san","marino","flag","nation","country","banner"],char:'🇸🇲',fitzpatrick_scale:false,category:"flags"},sao_tome_principe:{keywords:["sao","tome","principe","flag","nation","country","banner"],char:'🇸🇹',fitzpatrick_scale:false,category:"flags"},saudi_arabia:{keywords:["flag","nation","country","banner"],char:'🇸🇦',fitzpatrick_scale:false,category:"flags"},senegal:{keywords:["sn","flag","nation","country","banner"],char:'🇸🇳',fitzpatrick_scale:false,category:"flags"},serbia:{keywords:["rs","flag","nation","country","banner"],char:'🇷🇸',fitzpatrick_scale:false,category:"flags"},seychelles:{keywords:["sc","flag","nation","country","banner"],char:'🇸🇨',fitzpatrick_scale:false,category:"flags"},sierra_leone:{keywords:["sierra","leone","flag","nation","country","banner"],char:'🇸🇱',fitzpatrick_scale:false,category:"flags"},singapore:{keywords:["sg","flag","nation","country","banner"],char:'🇸🇬',fitzpatrick_scale:false,category:"flags"},sint_maarten:{keywords:["sint","maarten","dutch","flag","nation","country","banner"],char:'🇸🇽',fitzpatrick_scale:false,category:"flags"},slovakia:{keywords:["sk","flag","nation","country","banner"],char:'🇸🇰',fitzpatrick_scale:false,category:"flags"},slovenia:{keywords:["si","flag","nation","country","banner"],char:'🇸🇮',fitzpatrick_scale:false,category:"flags"},solomon_islands:{keywords:["solomon","islands","flag","nation","country","banner"],char:'🇸🇧',fitzpatrick_scale:false,category:"flags"},somalia:{keywords:["so","flag","nation","country","banner"],char:'🇸🇴',fitzpatrick_scale:false,category:"flags"},south_africa:{keywords:["south","africa","flag","nation","country","banner"],char:'🇿🇦',fitzpatrick_scale:false,category:"flags"},south_georgia_south_sandwich_islands:{keywords:["south","georgia","sandwich","islands","flag","nation","country","banner"],char:'🇬🇸',fitzpatrick_scale:false,category:"flags"},kr:{keywords:["south","korea","nation","flag","country","banner"],char:'🇰🇷',fitzpatrick_scale:false,category:"flags"},south_sudan:{keywords:["south","sd","flag","nation","country","banner"],char:'🇸🇸',fitzpatrick_scale:false,category:"flags"},es:{keywords:["spain","flag","nation","country","banner"],char:'🇪🇸',fitzpatrick_scale:false,category:"flags"},sri_lanka:{keywords:["sri","lanka","flag","nation","country","banner"],char:'🇱🇰',fitzpatrick_scale:false,category:"flags"},sudan:{keywords:["sd","flag","nation","country","banner"],char:'🇸🇩',fitzpatrick_scale:false,category:"flags"},suriname:{keywords:["sr","flag","nation","country","banner"],char:'🇸🇷',fitzpatrick_scale:false,category:"flags"},swaziland:{keywords:["sz","flag","nation","country","banner"],char:'🇸🇿',fitzpatrick_scale:false,category:"flags"},sweden:{keywords:["se","flag","nation","country","banner"],char:'🇸🇪',fitzpatrick_scale:false,category:"flags"},switzerland:{keywords:["ch","flag","nation","country","banner"],char:'🇨🇭',fitzpatrick_scale:false,category:"flags"},syria:{keywords:["syrian","arab","republic","flag","nation","country","banner"],char:'🇸🇾',fitzpatrick_scale:false,category:"flags"},taiwan:{keywords:["tw","flag","nation","country","banner"],char:'🇹🇼',fitzpatrick_scale:false,category:"flags"},tajikistan:{keywords:["tj","flag","nation","country","banner"],char:'🇹🇯',fitzpatrick_scale:false,category:"flags"},tanzania:{keywords:["tanzania,","united","republic","flag","nation","country","banner"],char:'🇹🇿',fitzpatrick_scale:false,category:"flags"},thailand:{keywords:["th","flag","nation","country","banner"],char:'🇹🇭',fitzpatrick_scale:false,category:"flags"},timor_leste:{keywords:["timor","leste","flag","nation","country","banner"],char:'🇹🇱',fitzpatrick_scale:false,category:"flags"},togo:{keywords:["tg","flag","nation","country","banner"],char:'🇹🇬',fitzpatrick_scale:false,category:"flags"},tokelau:{keywords:["tk","flag","nation","country","banner"],char:'🇹🇰',fitzpatrick_scale:false,category:"flags"},tonga:{keywords:["to","flag","nation","country","banner"],char:'🇹🇴',fitzpatrick_scale:false,category:"flags"},trinidad_tobago:{keywords:["trinidad","tobago","flag","nation","country","banner"],char:'🇹🇹',fitzpatrick_scale:false,category:"flags"},tunisia:{keywords:["tn","flag","nation","country","banner"],char:'🇹🇳',fitzpatrick_scale:false,category:"flags"},tr:{keywords:["turkey","flag","nation","country","banner"],char:'🇹🇷',fitzpatrick_scale:false,category:"flags"},turkmenistan:{keywords:["flag","nation","country","banner"],char:'🇹🇲',fitzpatrick_scale:false,category:"flags"},turks_caicos_islands:{keywords:["turks","caicos","islands","flag","nation","country","banner"],char:'🇹🇨',fitzpatrick_scale:false,category:"flags"},tuvalu:{keywords:["flag","nation","country","banner"],char:'🇹🇻',fitzpatrick_scale:false,category:"flags"},uganda:{keywords:["ug","flag","nation","country","banner"],char:'🇺🇬',fitzpatrick_scale:false,category:"flags"},ukraine:{keywords:["ua","flag","nation","country","banner"],char:'🇺🇦',fitzpatrick_scale:false,category:"flags"},united_arab_emirates:{keywords:["united","arab","emirates","flag","nation","country","banner"],char:'🇦🇪',fitzpatrick_scale:false,category:"flags"},uk:{keywords:["united","kingdom","great","britain","northern","ireland","flag","nation","country","banner","british","UK","english","england","union jack"],char:'🇬🇧',fitzpatrick_scale:false,category:"flags"},england:{keywords:["flag","english"],char:'🏴󠁧󠁢󠁥󠁮󠁧󠁿',fitzpatrick_scale:false,category:"flags"},scotland:{keywords:["flag","scottish"],char:'🏴󠁧󠁢󠁳󠁣󠁴󠁿',fitzpatrick_scale:false,category:"flags"},wales:{keywords:["flag","welsh"],char:'🏴󠁧󠁢󠁷󠁬󠁳󠁿',fitzpatrick_scale:false,category:"flags"},us:{keywords:["united","states","america","flag","nation","country","banner"],char:'🇺🇸',fitzpatrick_scale:false,category:"flags"},us_virgin_islands:{keywords:["virgin","islands","us","flag","nation","country","banner"],char:'🇻🇮',fitzpatrick_scale:false,category:"flags"},uruguay:{keywords:["uy","flag","nation","country","banner"],char:'🇺🇾',fitzpatrick_scale:false,category:"flags"},uzbekistan:{keywords:["uz","flag","nation","country","banner"],char:'🇺🇿',fitzpatrick_scale:false,category:"flags"},vanuatu:{keywords:["vu","flag","nation","country","banner"],char:'🇻🇺',fitzpatrick_scale:false,category:"flags"},vatican_city:{keywords:["vatican","city","flag","nation","country","banner"],char:'🇻🇦',fitzpatrick_scale:false,category:"flags"},venezuela:{keywords:["ve","bolivarian","republic","flag","nation","country","banner"],char:'🇻🇪',fitzpatrick_scale:false,category:"flags"},vietnam:{keywords:["viet","nam","flag","nation","country","banner"],char:'🇻🇳',fitzpatrick_scale:false,category:"flags"},wallis_futuna:{keywords:["wallis","futuna","flag","nation","country","banner"],char:'🇼🇫',fitzpatrick_scale:false,category:"flags"},western_sahara:{keywords:["western","sahara","flag","nation","country","banner"],char:'🇪🇭',fitzpatrick_scale:false,category:"flags"},yemen:{keywords:["ye","flag","nation","country","banner"],char:'🇾🇪',fitzpatrick_scale:false,category:"flags"},zambia:{keywords:["zm","flag","nation","country","banner"],char:'🇿🇲',fitzpatrick_scale:false,category:"flags"},zimbabwe:{keywords:["zw","flag","nation","country","banner"],char:'🇿🇼',fitzpatrick_scale:false,category:"flags"},united_nations:{keywords:["un","flag","banner"],char:'🇺🇳',fitzpatrick_scale:false,category:"flags"},pirate_flag:{keywords:["skull","crossbones","flag","banner"],char:'🏴‍☠️',fitzpatrick_scale:false,category:"flags"}}); ================================================ FILE: vue_acimage_web/public/tinymce/plugins/emoticons/js/emojis.js ================================================ window.tinymce.Resource.add("tinymce.plugins.emoticons",{grinning:{keywords:["face","smile","happy","joy",":D","grin"],char:"😀",fitzpatrick_scale:false,category:"people"},grimacing:{keywords:["face","grimace","teeth"],char:"😬",fitzpatrick_scale:false,category:"people"},grin:{keywords:["face","happy","smile","joy","kawaii"],char:"😁",fitzpatrick_scale:false,category:"people"},joy:{keywords:["face","cry","tears","weep","happy","happytears","haha"],char:"😂",fitzpatrick_scale:false,category:"people"},rofl:{keywords:["face","rolling","floor","laughing","lol","haha"],char:"🤣",fitzpatrick_scale:false,category:"people"},partying:{keywords:["face","celebration","woohoo"],char:"🥳",fitzpatrick_scale:false,category:"people"},smiley:{keywords:["face","happy","joy","haha",":D",":)","smile","funny"],char:"😃",fitzpatrick_scale:false,category:"people"},smile:{keywords:["face","happy","joy","funny","haha","laugh","like",":D",":)"],char:"😄",fitzpatrick_scale:false,category:"people"},sweat_smile:{keywords:["face","hot","happy","laugh","sweat","smile","relief"],char:"😅",fitzpatrick_scale:false,category:"people"},laughing:{keywords:["happy","joy","lol","satisfied","haha","face","glad","XD","laugh"],char:"😆",fitzpatrick_scale:false,category:"people"},innocent:{keywords:["face","angel","heaven","halo"],char:"😇",fitzpatrick_scale:false,category:"people"},wink:{keywords:["face","happy","mischievous","secret",";)","smile","eye"],char:"😉",fitzpatrick_scale:false,category:"people"},blush:{keywords:["face","smile","happy","flushed","crush","embarrassed","shy","joy"],char:"😊",fitzpatrick_scale:false,category:"people"},slightly_smiling_face:{keywords:["face","smile"],char:"🙂",fitzpatrick_scale:false,category:"people"},upside_down_face:{keywords:["face","flipped","silly","smile"],char:"🙃",fitzpatrick_scale:false,category:"people"},relaxed:{keywords:["face","blush","massage","happiness"],char:"☺️",fitzpatrick_scale:false,category:"people"},yum:{keywords:["happy","joy","tongue","smile","face","silly","yummy","nom","delicious","savouring"],char:"😋",fitzpatrick_scale:false,category:"people"},relieved:{keywords:["face","relaxed","phew","massage","happiness"],char:"😌",fitzpatrick_scale:false,category:"people"},heart_eyes:{keywords:["face","love","like","affection","valentines","infatuation","crush","heart"],char:"😍",fitzpatrick_scale:false,category:"people"},smiling_face_with_three_hearts:{keywords:["face","love","like","affection","valentines","infatuation","crush","hearts","adore"],char:"🥰",fitzpatrick_scale:false,category:"people"},kissing_heart:{keywords:["face","love","like","affection","valentines","infatuation","kiss"],char:"😘",fitzpatrick_scale:false,category:"people"},kissing:{keywords:["love","like","face","3","valentines","infatuation","kiss"],char:"😗",fitzpatrick_scale:false,category:"people"},kissing_smiling_eyes:{keywords:["face","affection","valentines","infatuation","kiss"],char:"😙",fitzpatrick_scale:false,category:"people"},kissing_closed_eyes:{keywords:["face","love","like","affection","valentines","infatuation","kiss"],char:"😚",fitzpatrick_scale:false,category:"people"},stuck_out_tongue_winking_eye:{keywords:["face","prank","childish","playful","mischievous","smile","wink","tongue"],char:"😜",fitzpatrick_scale:false,category:"people"},zany:{keywords:["face","goofy","crazy"],char:"🤪",fitzpatrick_scale:false,category:"people"},raised_eyebrow:{keywords:["face","distrust","scepticism","disapproval","disbelief","surprise"],char:"🤨",fitzpatrick_scale:false,category:"people"},monocle:{keywords:["face","stuffy","wealthy"],char:"🧐",fitzpatrick_scale:false,category:"people"},stuck_out_tongue_closed_eyes:{keywords:["face","prank","playful","mischievous","smile","tongue"],char:"😝",fitzpatrick_scale:false,category:"people"},stuck_out_tongue:{keywords:["face","prank","childish","playful","mischievous","smile","tongue"],char:"😛",fitzpatrick_scale:false,category:"people"},money_mouth_face:{keywords:["face","rich","dollar","money"],char:"🤑",fitzpatrick_scale:false,category:"people"},nerd_face:{keywords:["face","nerdy","geek","dork"],char:"🤓",fitzpatrick_scale:false,category:"people"},sunglasses:{keywords:["face","cool","smile","summer","beach","sunglass"],char:"😎",fitzpatrick_scale:false,category:"people"},star_struck:{keywords:["face","smile","starry","eyes","grinning"],char:"🤩",fitzpatrick_scale:false,category:"people"},clown_face:{keywords:["face"],char:"🤡",fitzpatrick_scale:false,category:"people"},cowboy_hat_face:{keywords:["face","cowgirl","hat"],char:"🤠",fitzpatrick_scale:false,category:"people"},hugs:{keywords:["face","smile","hug"],char:"🤗",fitzpatrick_scale:false,category:"people"},smirk:{keywords:["face","smile","mean","prank","smug","sarcasm"],char:"😏",fitzpatrick_scale:false,category:"people"},no_mouth:{keywords:["face","hellokitty"],char:"😶",fitzpatrick_scale:false,category:"people"},neutral_face:{keywords:["indifference","meh",":|","neutral"],char:"😐",fitzpatrick_scale:false,category:"people"},expressionless:{keywords:["face","indifferent","-_-","meh","deadpan"],char:"😑",fitzpatrick_scale:false,category:"people"},unamused:{keywords:["indifference","bored","straight face","serious","sarcasm","unimpressed","skeptical","dubious","side_eye"],char:"😒",fitzpatrick_scale:false,category:"people"},roll_eyes:{keywords:["face","eyeroll","frustrated"],char:"🙄",fitzpatrick_scale:false,category:"people"},thinking:{keywords:["face","hmmm","think","consider"],char:"🤔",fitzpatrick_scale:false,category:"people"},lying_face:{keywords:["face","lie","pinocchio"],char:"🤥",fitzpatrick_scale:false,category:"people"},hand_over_mouth:{keywords:["face","whoops","shock","surprise"],char:"🤭",fitzpatrick_scale:false,category:"people"},shushing:{keywords:["face","quiet","shhh"],char:"🤫",fitzpatrick_scale:false,category:"people"},symbols_over_mouth:{keywords:["face","swearing","cursing","cussing","profanity","expletive"],char:"🤬",fitzpatrick_scale:false,category:"people"},exploding_head:{keywords:["face","shocked","mind","blown"],char:"🤯",fitzpatrick_scale:false,category:"people"},flushed:{keywords:["face","blush","shy","flattered"],char:"😳",fitzpatrick_scale:false,category:"people"},disappointed:{keywords:["face","sad","upset","depressed",":("],char:"😞",fitzpatrick_scale:false,category:"people"},worried:{keywords:["face","concern","nervous",":("],char:"😟",fitzpatrick_scale:false,category:"people"},angry:{keywords:["mad","face","annoyed","frustrated"],char:"😠",fitzpatrick_scale:false,category:"people"},rage:{keywords:["angry","mad","hate","despise"],char:"😡",fitzpatrick_scale:false,category:"people"},pensive:{keywords:["face","sad","depressed","upset"],char:"😔",fitzpatrick_scale:false,category:"people"},confused:{keywords:["face","indifference","huh","weird","hmmm",":/"],char:"😕",fitzpatrick_scale:false,category:"people"},slightly_frowning_face:{keywords:["face","frowning","disappointed","sad","upset"],char:"🙁",fitzpatrick_scale:false,category:"people"},frowning_face:{keywords:["face","sad","upset","frown"],char:"☹",fitzpatrick_scale:false,category:"people"},persevere:{keywords:["face","sick","no","upset","oops"],char:"😣",fitzpatrick_scale:false,category:"people"},confounded:{keywords:["face","confused","sick","unwell","oops",":S"],char:"😖",fitzpatrick_scale:false,category:"people"},tired_face:{keywords:["sick","whine","upset","frustrated"],char:"😫",fitzpatrick_scale:false,category:"people"},weary:{keywords:["face","tired","sleepy","sad","frustrated","upset"],char:"😩",fitzpatrick_scale:false,category:"people"},pleading:{keywords:["face","begging","mercy"],char:"🥺",fitzpatrick_scale:false,category:"people"},triumph:{keywords:["face","gas","phew","proud","pride"],char:"😤",fitzpatrick_scale:false,category:"people"},open_mouth:{keywords:["face","surprise","impressed","wow","whoa",":O"],char:"😮",fitzpatrick_scale:false,category:"people"},scream:{keywords:["face","munch","scared","omg"],char:"😱",fitzpatrick_scale:false,category:"people"},fearful:{keywords:["face","scared","terrified","nervous","oops","huh"],char:"😨",fitzpatrick_scale:false,category:"people"},cold_sweat:{keywords:["face","nervous","sweat"],char:"😰",fitzpatrick_scale:false,category:"people"},hushed:{keywords:["face","woo","shh"],char:"😯",fitzpatrick_scale:false,category:"people"},frowning:{keywords:["face","aw","what"],char:"😦",fitzpatrick_scale:false,category:"people"},anguished:{keywords:["face","stunned","nervous"],char:"😧",fitzpatrick_scale:false,category:"people"},cry:{keywords:["face","tears","sad","depressed","upset",":'("],char:"😢",fitzpatrick_scale:false,category:"people"},disappointed_relieved:{keywords:["face","phew","sweat","nervous"],char:"😥",fitzpatrick_scale:false,category:"people"},drooling_face:{keywords:["face"],char:"🤤",fitzpatrick_scale:false,category:"people"},sleepy:{keywords:["face","tired","rest","nap"],char:"😪",fitzpatrick_scale:false,category:"people"},sweat:{keywords:["face","hot","sad","tired","exercise"],char:"😓",fitzpatrick_scale:false,category:"people"},hot:{keywords:["face","feverish","heat","red","sweating"],char:"🥵",fitzpatrick_scale:false,category:"people"},cold:{keywords:["face","blue","freezing","frozen","frostbite","icicles"],char:"🥶",fitzpatrick_scale:false,category:"people"},sob:{keywords:["face","cry","tears","sad","upset","depressed"],char:"😭",fitzpatrick_scale:false,category:"people"},dizzy_face:{keywords:["spent","unconscious","xox","dizzy"],char:"😵",fitzpatrick_scale:false,category:"people"},astonished:{keywords:["face","xox","surprised","poisoned"],char:"😲",fitzpatrick_scale:false,category:"people"},zipper_mouth_face:{keywords:["face","sealed","zipper","secret"],char:"🤐",fitzpatrick_scale:false,category:"people"},nauseated_face:{keywords:["face","vomit","gross","green","sick","throw up","ill"],char:"🤢",fitzpatrick_scale:false,category:"people"},sneezing_face:{keywords:["face","gesundheit","sneeze","sick","allergy"],char:"🤧",fitzpatrick_scale:false,category:"people"},vomiting:{keywords:["face","sick"],char:"🤮",fitzpatrick_scale:false,category:"people"},mask:{keywords:["face","sick","ill","disease"],char:"😷",fitzpatrick_scale:false,category:"people"},face_with_thermometer:{keywords:["sick","temperature","thermometer","cold","fever"],char:"🤒",fitzpatrick_scale:false,category:"people"},face_with_head_bandage:{keywords:["injured","clumsy","bandage","hurt"],char:"🤕",fitzpatrick_scale:false,category:"people"},woozy:{keywords:["face","dizzy","intoxicated","tipsy","wavy"],char:"🥴",fitzpatrick_scale:false,category:"people"},sleeping:{keywords:["face","tired","sleepy","night","zzz"],char:"😴",fitzpatrick_scale:false,category:"people"},zzz:{keywords:["sleepy","tired","dream"],char:"💤",fitzpatrick_scale:false,category:"people"},poop:{keywords:["hankey","shitface","fail","turd","shit"],char:"💩",fitzpatrick_scale:false,category:"people"},smiling_imp:{keywords:["devil","horns"],char:"😈",fitzpatrick_scale:false,category:"people"},imp:{keywords:["devil","angry","horns"],char:"👿",fitzpatrick_scale:false,category:"people"},japanese_ogre:{keywords:["monster","red","mask","halloween","scary","creepy","devil","demon","japanese","ogre"],char:"👹",fitzpatrick_scale:false,category:"people"},japanese_goblin:{keywords:["red","evil","mask","monster","scary","creepy","japanese","goblin"],char:"👺",fitzpatrick_scale:false,category:"people"},skull:{keywords:["dead","skeleton","creepy","death"],char:"💀",fitzpatrick_scale:false,category:"people"},ghost:{keywords:["halloween","spooky","scary"],char:"👻",fitzpatrick_scale:false,category:"people"},alien:{keywords:["UFO","paul","weird","outer_space"],char:"👽",fitzpatrick_scale:false,category:"people"},robot:{keywords:["computer","machine","bot"],char:"🤖",fitzpatrick_scale:false,category:"people"},smiley_cat:{keywords:["animal","cats","happy","smile"],char:"😺",fitzpatrick_scale:false,category:"people"},smile_cat:{keywords:["animal","cats","smile"],char:"😸",fitzpatrick_scale:false,category:"people"},joy_cat:{keywords:["animal","cats","haha","happy","tears"],char:"😹",fitzpatrick_scale:false,category:"people"},heart_eyes_cat:{keywords:["animal","love","like","affection","cats","valentines","heart"],char:"😻",fitzpatrick_scale:false,category:"people"},smirk_cat:{keywords:["animal","cats","smirk"],char:"😼",fitzpatrick_scale:false,category:"people"},kissing_cat:{keywords:["animal","cats","kiss"],char:"😽",fitzpatrick_scale:false,category:"people"},scream_cat:{keywords:["animal","cats","munch","scared","scream"],char:"🙀",fitzpatrick_scale:false,category:"people"},crying_cat_face:{keywords:["animal","tears","weep","sad","cats","upset","cry"],char:"😿",fitzpatrick_scale:false,category:"people"},pouting_cat:{keywords:["animal","cats"],char:"😾",fitzpatrick_scale:false,category:"people"},palms_up:{keywords:["hands","gesture","cupped","prayer"],char:"🤲",fitzpatrick_scale:true,category:"people"},raised_hands:{keywords:["gesture","hooray","yea","celebration","hands"],char:"🙌",fitzpatrick_scale:true,category:"people"},clap:{keywords:["hands","praise","applause","congrats","yay"],char:"👏",fitzpatrick_scale:true,category:"people"},wave:{keywords:["hands","gesture","goodbye","solong","farewell","hello","hi","palm"],char:"👋",fitzpatrick_scale:true,category:"people"},call_me_hand:{keywords:["hands","gesture"],char:"🤙",fitzpatrick_scale:true,category:"people"},"+1":{keywords:["thumbsup","yes","awesome","good","agree","accept","cool","hand","like"],char:"👍",fitzpatrick_scale:true,category:"people"},"-1":{keywords:["thumbsdown","no","dislike","hand"],char:"👎",fitzpatrick_scale:true,category:"people"},facepunch:{keywords:["angry","violence","fist","hit","attack","hand"],char:"👊",fitzpatrick_scale:true,category:"people"},fist:{keywords:["fingers","hand","grasp"],char:"✊",fitzpatrick_scale:true,category:"people"},fist_left:{keywords:["hand","fistbump"],char:"🤛",fitzpatrick_scale:true,category:"people"},fist_right:{keywords:["hand","fistbump"],char:"🤜",fitzpatrick_scale:true,category:"people"},v:{keywords:["fingers","ohyeah","hand","peace","victory","two"],char:"✌",fitzpatrick_scale:true,category:"people"},ok_hand:{keywords:["fingers","limbs","perfect","ok","okay"],char:"👌",fitzpatrick_scale:true,category:"people"},raised_hand:{keywords:["fingers","stop","highfive","palm","ban"],char:"✋",fitzpatrick_scale:true,category:"people"},raised_back_of_hand:{keywords:["fingers","raised","backhand"],char:"🤚",fitzpatrick_scale:true,category:"people"},open_hands:{keywords:["fingers","butterfly","hands","open"],char:"👐",fitzpatrick_scale:true,category:"people"},muscle:{keywords:["arm","flex","hand","summer","strong","biceps"],char:"💪",fitzpatrick_scale:true,category:"people"},pray:{keywords:["please","hope","wish","namaste","highfive"],char:"🙏",fitzpatrick_scale:true,category:"people"},foot:{keywords:["kick","stomp"],char:"🦶",fitzpatrick_scale:true,category:"people"},leg:{keywords:["kick","limb"],char:"🦵",fitzpatrick_scale:true,category:"people"},handshake:{keywords:["agreement","shake"],char:"🤝",fitzpatrick_scale:false,category:"people"},point_up:{keywords:["hand","fingers","direction","up"],char:"☝",fitzpatrick_scale:true,category:"people"},point_up_2:{keywords:["fingers","hand","direction","up"],char:"👆",fitzpatrick_scale:true,category:"people"},point_down:{keywords:["fingers","hand","direction","down"],char:"👇",fitzpatrick_scale:true,category:"people"},point_left:{keywords:["direction","fingers","hand","left"],char:"👈",fitzpatrick_scale:true,category:"people"},point_right:{keywords:["fingers","hand","direction","right"],char:"👉",fitzpatrick_scale:true,category:"people"},fu:{keywords:["hand","fingers","rude","middle","flipping"],char:"🖕",fitzpatrick_scale:true,category:"people"},raised_hand_with_fingers_splayed:{keywords:["hand","fingers","palm"],char:"🖐",fitzpatrick_scale:true,category:"people"},love_you:{keywords:["hand","fingers","gesture"],char:"🤟",fitzpatrick_scale:true,category:"people"},metal:{keywords:["hand","fingers","evil_eye","sign_of_horns","rock_on"],char:"🤘",fitzpatrick_scale:true,category:"people"},crossed_fingers:{keywords:["good","lucky"],char:"🤞",fitzpatrick_scale:true,category:"people"},vulcan_salute:{keywords:["hand","fingers","spock","star trek"],char:"🖖",fitzpatrick_scale:true,category:"people"},writing_hand:{keywords:["lower_left_ballpoint_pen","stationery","write","compose"],char:"✍",fitzpatrick_scale:true,category:"people"},selfie:{keywords:["camera","phone"],char:"🤳",fitzpatrick_scale:true,category:"people"},nail_care:{keywords:["beauty","manicure","finger","fashion","nail"],char:"💅",fitzpatrick_scale:true,category:"people"},lips:{keywords:["mouth","kiss"],char:"👄",fitzpatrick_scale:false,category:"people"},tooth:{keywords:["teeth","dentist"],char:"🦷",fitzpatrick_scale:false,category:"people"},tongue:{keywords:["mouth","playful"],char:"👅",fitzpatrick_scale:false,category:"people"},ear:{keywords:["face","hear","sound","listen"],char:"👂",fitzpatrick_scale:true,category:"people"},nose:{keywords:["smell","sniff"],char:"👃",fitzpatrick_scale:true,category:"people"},eye:{keywords:["face","look","see","watch","stare"],char:"👁",fitzpatrick_scale:false,category:"people"},eyes:{keywords:["look","watch","stalk","peek","see"],char:"👀",fitzpatrick_scale:false,category:"people"},brain:{keywords:["smart","intelligent"],char:"🧠",fitzpatrick_scale:false,category:"people"},bust_in_silhouette:{keywords:["user","person","human"],char:"👤",fitzpatrick_scale:false,category:"people"},busts_in_silhouette:{keywords:["user","person","human","group","team"],char:"👥",fitzpatrick_scale:false,category:"people"},speaking_head:{keywords:["user","person","human","sing","say","talk"],char:"🗣",fitzpatrick_scale:false,category:"people"},baby:{keywords:["child","boy","girl","toddler"],char:"👶",fitzpatrick_scale:true,category:"people"},child:{keywords:["gender-neutral","young"],char:"🧒",fitzpatrick_scale:true,category:"people"},boy:{keywords:["man","male","guy","teenager"],char:"👦",fitzpatrick_scale:true,category:"people"},girl:{keywords:["female","woman","teenager"],char:"👧",fitzpatrick_scale:true,category:"people"},adult:{keywords:["gender-neutral","person"],char:"🧑",fitzpatrick_scale:true,category:"people"},man:{keywords:["mustache","father","dad","guy","classy","sir","moustache"],char:"👨",fitzpatrick_scale:true,category:"people"},woman:{keywords:["female","girls","lady"],char:"👩",fitzpatrick_scale:true,category:"people"},blonde_woman:{keywords:["woman","female","girl","blonde","person"],char:"👱‍♀️",fitzpatrick_scale:true,category:"people"},blonde_man:{keywords:["man","male","boy","blonde","guy","person"],char:"👱",fitzpatrick_scale:true,category:"people"},bearded_person:{keywords:["person","bewhiskered"],char:"🧔",fitzpatrick_scale:true,category:"people"},older_adult:{keywords:["human","elder","senior","gender-neutral"],char:"🧓",fitzpatrick_scale:true,category:"people"},older_man:{keywords:["human","male","men","old","elder","senior"],char:"👴",fitzpatrick_scale:true,category:"people"},older_woman:{keywords:["human","female","women","lady","old","elder","senior"],char:"👵",fitzpatrick_scale:true,category:"people"},man_with_gua_pi_mao:{keywords:["male","boy","chinese"],char:"👲",fitzpatrick_scale:true,category:"people"},woman_with_headscarf:{keywords:["female","hijab","mantilla","tichel"],char:"🧕",fitzpatrick_scale:true,category:"people"},woman_with_turban:{keywords:["female","indian","hinduism","arabs","woman"],char:"👳‍♀️",fitzpatrick_scale:true,category:"people"},man_with_turban:{keywords:["male","indian","hinduism","arabs"],char:"👳",fitzpatrick_scale:true,category:"people"},policewoman:{keywords:["woman","police","law","legal","enforcement","arrest","911","female"],char:"👮‍♀️",fitzpatrick_scale:true,category:"people"},policeman:{keywords:["man","police","law","legal","enforcement","arrest","911"],char:"👮",fitzpatrick_scale:true,category:"people"},construction_worker_woman:{keywords:["female","human","wip","build","construction","worker","labor","woman"],char:"👷‍♀️",fitzpatrick_scale:true,category:"people"},construction_worker_man:{keywords:["male","human","wip","guy","build","construction","worker","labor"],char:"👷",fitzpatrick_scale:true,category:"people"},guardswoman:{keywords:["uk","gb","british","female","royal","woman"],char:"💂‍♀️",fitzpatrick_scale:true,category:"people"},guardsman:{keywords:["uk","gb","british","male","guy","royal"],char:"💂",fitzpatrick_scale:true,category:"people"},female_detective:{keywords:["human","spy","detective","female","woman"],char:"🕵️‍♀️",fitzpatrick_scale:true,category:"people"},male_detective:{keywords:["human","spy","detective"],char:"🕵",fitzpatrick_scale:true,category:"people"},woman_health_worker:{keywords:["doctor","nurse","therapist","healthcare","woman","human"],char:"👩‍⚕️",fitzpatrick_scale:true,category:"people"},man_health_worker:{keywords:["doctor","nurse","therapist","healthcare","man","human"],char:"👨‍⚕️",fitzpatrick_scale:true,category:"people"},woman_farmer:{keywords:["rancher","gardener","woman","human"],char:"👩‍🌾",fitzpatrick_scale:true,category:"people"},man_farmer:{keywords:["rancher","gardener","man","human"],char:"👨‍🌾",fitzpatrick_scale:true,category:"people"},woman_cook:{keywords:["chef","woman","human"],char:"👩‍🍳",fitzpatrick_scale:true,category:"people"},man_cook:{keywords:["chef","man","human"],char:"👨‍🍳",fitzpatrick_scale:true,category:"people"},woman_student:{keywords:["graduate","woman","human"],char:"👩‍🎓",fitzpatrick_scale:true,category:"people"},man_student:{keywords:["graduate","man","human"],char:"👨‍🎓",fitzpatrick_scale:true,category:"people"},woman_singer:{keywords:["rockstar","entertainer","woman","human"],char:"👩‍🎤",fitzpatrick_scale:true,category:"people"},man_singer:{keywords:["rockstar","entertainer","man","human"],char:"👨‍🎤",fitzpatrick_scale:true,category:"people"},woman_teacher:{keywords:["instructor","professor","woman","human"],char:"👩‍🏫",fitzpatrick_scale:true,category:"people"},man_teacher:{keywords:["instructor","professor","man","human"],char:"👨‍🏫",fitzpatrick_scale:true,category:"people"},woman_factory_worker:{keywords:["assembly","industrial","woman","human"],char:"👩‍🏭",fitzpatrick_scale:true,category:"people"},man_factory_worker:{keywords:["assembly","industrial","man","human"],char:"👨‍🏭",fitzpatrick_scale:true,category:"people"},woman_technologist:{keywords:["coder","developer","engineer","programmer","software","woman","human","laptop","computer"],char:"👩‍💻",fitzpatrick_scale:true,category:"people"},man_technologist:{keywords:["coder","developer","engineer","programmer","software","man","human","laptop","computer"],char:"👨‍💻",fitzpatrick_scale:true,category:"people"},woman_office_worker:{keywords:["business","manager","woman","human"],char:"👩‍💼",fitzpatrick_scale:true,category:"people"},man_office_worker:{keywords:["business","manager","man","human"],char:"👨‍💼",fitzpatrick_scale:true,category:"people"},woman_mechanic:{keywords:["plumber","woman","human","wrench"],char:"👩‍🔧",fitzpatrick_scale:true,category:"people"},man_mechanic:{keywords:["plumber","man","human","wrench"],char:"👨‍🔧",fitzpatrick_scale:true,category:"people"},woman_scientist:{keywords:["biologist","chemist","engineer","physicist","woman","human"],char:"👩‍🔬",fitzpatrick_scale:true,category:"people"},man_scientist:{keywords:["biologist","chemist","engineer","physicist","man","human"],char:"👨‍🔬",fitzpatrick_scale:true,category:"people"},woman_artist:{keywords:["painter","woman","human"],char:"👩‍🎨",fitzpatrick_scale:true,category:"people"},man_artist:{keywords:["painter","man","human"],char:"👨‍🎨",fitzpatrick_scale:true,category:"people"},woman_firefighter:{keywords:["fireman","woman","human"],char:"👩‍🚒",fitzpatrick_scale:true,category:"people"},man_firefighter:{keywords:["fireman","man","human"],char:"👨‍🚒",fitzpatrick_scale:true,category:"people"},woman_pilot:{keywords:["aviator","plane","woman","human"],char:"👩‍✈️",fitzpatrick_scale:true,category:"people"},man_pilot:{keywords:["aviator","plane","man","human"],char:"👨‍✈️",fitzpatrick_scale:true,category:"people"},woman_astronaut:{keywords:["space","rocket","woman","human"],char:"👩‍🚀",fitzpatrick_scale:true,category:"people"},man_astronaut:{keywords:["space","rocket","man","human"],char:"👨‍🚀",fitzpatrick_scale:true,category:"people"},woman_judge:{keywords:["justice","court","woman","human"],char:"👩‍⚖️",fitzpatrick_scale:true,category:"people"},man_judge:{keywords:["justice","court","man","human"],char:"👨‍⚖️",fitzpatrick_scale:true,category:"people"},woman_superhero:{keywords:["woman","female","good","heroine","superpowers"],char:"🦸‍♀️",fitzpatrick_scale:true,category:"people"},man_superhero:{keywords:["man","male","good","hero","superpowers"],char:"🦸‍♂️",fitzpatrick_scale:true,category:"people"},woman_supervillain:{keywords:["woman","female","evil","bad","criminal","heroine","superpowers"],char:"🦹‍♀️",fitzpatrick_scale:true,category:"people"},man_supervillain:{keywords:["man","male","evil","bad","criminal","hero","superpowers"],char:"🦹‍♂️",fitzpatrick_scale:true,category:"people"},mrs_claus:{keywords:["woman","female","xmas","mother christmas"],char:"🤶",fitzpatrick_scale:true,category:"people"},santa:{keywords:["festival","man","male","xmas","father christmas"],char:"🎅",fitzpatrick_scale:true,category:"people"},sorceress:{keywords:["woman","female","mage","witch"],char:"🧙‍♀️",fitzpatrick_scale:true,category:"people"},wizard:{keywords:["man","male","mage","sorcerer"],char:"🧙‍♂️",fitzpatrick_scale:true,category:"people"},woman_elf:{keywords:["woman","female"],char:"🧝‍♀️",fitzpatrick_scale:true,category:"people"},man_elf:{keywords:["man","male"],char:"🧝‍♂️",fitzpatrick_scale:true,category:"people"},woman_vampire:{keywords:["woman","female"],char:"🧛‍♀️",fitzpatrick_scale:true,category:"people"},man_vampire:{keywords:["man","male","dracula"],char:"🧛‍♂️",fitzpatrick_scale:true,category:"people"},woman_zombie:{keywords:["woman","female","undead","walking dead"],char:"🧟‍♀️",fitzpatrick_scale:false,category:"people"},man_zombie:{keywords:["man","male","dracula","undead","walking dead"],char:"🧟‍♂️",fitzpatrick_scale:false,category:"people"},woman_genie:{keywords:["woman","female"],char:"🧞‍♀️",fitzpatrick_scale:false,category:"people"},man_genie:{keywords:["man","male"],char:"🧞‍♂️",fitzpatrick_scale:false,category:"people"},mermaid:{keywords:["woman","female","merwoman","ariel"],char:"🧜‍♀️",fitzpatrick_scale:true,category:"people"},merman:{keywords:["man","male","triton"],char:"🧜‍♂️",fitzpatrick_scale:true,category:"people"},woman_fairy:{keywords:["woman","female"],char:"🧚‍♀️",fitzpatrick_scale:true,category:"people"},man_fairy:{keywords:["man","male"],char:"🧚‍♂️",fitzpatrick_scale:true,category:"people"},angel:{keywords:["heaven","wings","halo"],char:"👼",fitzpatrick_scale:true,category:"people"},pregnant_woman:{keywords:["baby"],char:"🤰",fitzpatrick_scale:true,category:"people"},breastfeeding:{keywords:["nursing","baby"],char:"🤱",fitzpatrick_scale:true,category:"people"},princess:{keywords:["girl","woman","female","blond","crown","royal","queen"],char:"👸",fitzpatrick_scale:true,category:"people"},prince:{keywords:["boy","man","male","crown","royal","king"],char:"🤴",fitzpatrick_scale:true,category:"people"},bride_with_veil:{keywords:["couple","marriage","wedding","woman","bride"],char:"👰",fitzpatrick_scale:true,category:"people"},man_in_tuxedo:{keywords:["couple","marriage","wedding","groom"],char:"🤵",fitzpatrick_scale:true,category:"people"},running_woman:{keywords:["woman","walking","exercise","race","running","female"],char:"🏃‍♀️",fitzpatrick_scale:true,category:"people"},running_man:{keywords:["man","walking","exercise","race","running"],char:"🏃",fitzpatrick_scale:true,category:"people"},walking_woman:{keywords:["human","feet","steps","woman","female"],char:"🚶‍♀️",fitzpatrick_scale:true,category:"people"},walking_man:{keywords:["human","feet","steps"],char:"🚶",fitzpatrick_scale:true,category:"people"},dancer:{keywords:["female","girl","woman","fun"],char:"💃",fitzpatrick_scale:true,category:"people"},man_dancing:{keywords:["male","boy","fun","dancer"],char:"🕺",fitzpatrick_scale:true,category:"people"},dancing_women:{keywords:["female","bunny","women","girls"],char:"👯",fitzpatrick_scale:false,category:"people"},dancing_men:{keywords:["male","bunny","men","boys"],char:"👯‍♂️",fitzpatrick_scale:false,category:"people"},couple:{keywords:["pair","people","human","love","date","dating","like","affection","valentines","marriage"],char:"👫",fitzpatrick_scale:false,category:"people"},two_men_holding_hands:{keywords:["pair","couple","love","like","bromance","friendship","people","human"],char:"👬",fitzpatrick_scale:false,category:"people"},two_women_holding_hands:{keywords:["pair","friendship","couple","love","like","female","people","human"],char:"👭",fitzpatrick_scale:false,category:"people"},bowing_woman:{keywords:["woman","female","girl"],char:"🙇‍♀️",fitzpatrick_scale:true,category:"people"},bowing_man:{keywords:["man","male","boy"],char:"🙇",fitzpatrick_scale:true,category:"people"},man_facepalming:{keywords:["man","male","boy","disbelief"],char:"🤦‍♂️",fitzpatrick_scale:true,category:"people"},woman_facepalming:{keywords:["woman","female","girl","disbelief"],char:"🤦‍♀️",fitzpatrick_scale:true,category:"people"},woman_shrugging:{keywords:["woman","female","girl","confused","indifferent","doubt"],char:"🤷",fitzpatrick_scale:true,category:"people"},man_shrugging:{keywords:["man","male","boy","confused","indifferent","doubt"],char:"🤷‍♂️",fitzpatrick_scale:true,category:"people"},tipping_hand_woman:{keywords:["female","girl","woman","human","information"],char:"💁",fitzpatrick_scale:true,category:"people"},tipping_hand_man:{keywords:["male","boy","man","human","information"],char:"💁‍♂️",fitzpatrick_scale:true,category:"people"},no_good_woman:{keywords:["female","girl","woman","nope"],char:"🙅",fitzpatrick_scale:true,category:"people"},no_good_man:{keywords:["male","boy","man","nope"],char:"🙅‍♂️",fitzpatrick_scale:true,category:"people"},ok_woman:{keywords:["women","girl","female","pink","human","woman"],char:"🙆",fitzpatrick_scale:true,category:"people"},ok_man:{keywords:["men","boy","male","blue","human","man"],char:"🙆‍♂️",fitzpatrick_scale:true,category:"people"},raising_hand_woman:{keywords:["female","girl","woman"],char:"🙋",fitzpatrick_scale:true,category:"people"},raising_hand_man:{keywords:["male","boy","man"],char:"🙋‍♂️",fitzpatrick_scale:true,category:"people"},pouting_woman:{keywords:["female","girl","woman"],char:"🙎",fitzpatrick_scale:true,category:"people"},pouting_man:{keywords:["male","boy","man"],char:"🙎‍♂️",fitzpatrick_scale:true,category:"people"},frowning_woman:{keywords:["female","girl","woman","sad","depressed","discouraged","unhappy"],char:"🙍",fitzpatrick_scale:true,category:"people"},frowning_man:{keywords:["male","boy","man","sad","depressed","discouraged","unhappy"],char:"🙍‍♂️",fitzpatrick_scale:true,category:"people"},haircut_woman:{keywords:["female","girl","woman"],char:"💇",fitzpatrick_scale:true,category:"people"},haircut_man:{keywords:["male","boy","man"],char:"💇‍♂️",fitzpatrick_scale:true,category:"people"},massage_woman:{keywords:["female","girl","woman","head"],char:"💆",fitzpatrick_scale:true,category:"people"},massage_man:{keywords:["male","boy","man","head"],char:"💆‍♂️",fitzpatrick_scale:true,category:"people"},woman_in_steamy_room:{keywords:["female","woman","spa","steamroom","sauna"],char:"🧖‍♀️",fitzpatrick_scale:true,category:"people"},man_in_steamy_room:{keywords:["male","man","spa","steamroom","sauna"],char:"🧖‍♂️",fitzpatrick_scale:true,category:"people"},couple_with_heart_woman_man:{keywords:["pair","love","like","affection","human","dating","valentines","marriage"],char:"💑",fitzpatrick_scale:false,category:"people"},couple_with_heart_woman_woman:{keywords:["pair","love","like","affection","human","dating","valentines","marriage"],char:"👩‍❤️‍👩",fitzpatrick_scale:false,category:"people"},couple_with_heart_man_man:{keywords:["pair","love","like","affection","human","dating","valentines","marriage"],char:"👨‍❤️‍👨",fitzpatrick_scale:false,category:"people"},couplekiss_man_woman:{keywords:["pair","valentines","love","like","dating","marriage"],char:"💏",fitzpatrick_scale:false,category:"people"},couplekiss_woman_woman:{keywords:["pair","valentines","love","like","dating","marriage"],char:"👩‍❤️‍💋‍👩",fitzpatrick_scale:false,category:"people"},couplekiss_man_man:{keywords:["pair","valentines","love","like","dating","marriage"],char:"👨‍❤️‍💋‍👨",fitzpatrick_scale:false,category:"people"},family_man_woman_boy:{keywords:["home","parents","child","mom","dad","father","mother","people","human"],char:"👪",fitzpatrick_scale:false,category:"people"},family_man_woman_girl:{keywords:["home","parents","people","human","child"],char:"👨‍👩‍👧",fitzpatrick_scale:false,category:"people"},family_man_woman_girl_boy:{keywords:["home","parents","people","human","children"],char:"👨‍👩‍👧‍👦",fitzpatrick_scale:false,category:"people"},family_man_woman_boy_boy:{keywords:["home","parents","people","human","children"],char:"👨‍👩‍👦‍👦",fitzpatrick_scale:false,category:"people"},family_man_woman_girl_girl:{keywords:["home","parents","people","human","children"],char:"👨‍👩‍👧‍👧",fitzpatrick_scale:false,category:"people"},family_woman_woman_boy:{keywords:["home","parents","people","human","children"],char:"👩‍👩‍👦",fitzpatrick_scale:false,category:"people"},family_woman_woman_girl:{keywords:["home","parents","people","human","children"],char:"👩‍👩‍👧",fitzpatrick_scale:false,category:"people"},family_woman_woman_girl_boy:{keywords:["home","parents","people","human","children"],char:"👩‍👩‍👧‍👦",fitzpatrick_scale:false,category:"people"},family_woman_woman_boy_boy:{keywords:["home","parents","people","human","children"],char:"👩‍👩‍👦‍👦",fitzpatrick_scale:false,category:"people"},family_woman_woman_girl_girl:{keywords:["home","parents","people","human","children"],char:"👩‍👩‍👧‍👧",fitzpatrick_scale:false,category:"people"},family_man_man_boy:{keywords:["home","parents","people","human","children"],char:"👨‍👨‍👦",fitzpatrick_scale:false,category:"people"},family_man_man_girl:{keywords:["home","parents","people","human","children"],char:"👨‍👨‍👧",fitzpatrick_scale:false,category:"people"},family_man_man_girl_boy:{keywords:["home","parents","people","human","children"],char:"👨‍👨‍👧‍👦",fitzpatrick_scale:false,category:"people"},family_man_man_boy_boy:{keywords:["home","parents","people","human","children"],char:"👨‍👨‍👦‍👦",fitzpatrick_scale:false,category:"people"},family_man_man_girl_girl:{keywords:["home","parents","people","human","children"],char:"👨‍👨‍👧‍👧",fitzpatrick_scale:false,category:"people"},family_woman_boy:{keywords:["home","parent","people","human","child"],char:"👩‍👦",fitzpatrick_scale:false,category:"people"},family_woman_girl:{keywords:["home","parent","people","human","child"],char:"👩‍👧",fitzpatrick_scale:false,category:"people"},family_woman_girl_boy:{keywords:["home","parent","people","human","children"],char:"👩‍👧‍👦",fitzpatrick_scale:false,category:"people"},family_woman_boy_boy:{keywords:["home","parent","people","human","children"],char:"👩‍👦‍👦",fitzpatrick_scale:false,category:"people"},family_woman_girl_girl:{keywords:["home","parent","people","human","children"],char:"👩‍👧‍👧",fitzpatrick_scale:false,category:"people"},family_man_boy:{keywords:["home","parent","people","human","child"],char:"👨‍👦",fitzpatrick_scale:false,category:"people"},family_man_girl:{keywords:["home","parent","people","human","child"],char:"👨‍👧",fitzpatrick_scale:false,category:"people"},family_man_girl_boy:{keywords:["home","parent","people","human","children"],char:"👨‍👧‍👦",fitzpatrick_scale:false,category:"people"},family_man_boy_boy:{keywords:["home","parent","people","human","children"],char:"👨‍👦‍👦",fitzpatrick_scale:false,category:"people"},family_man_girl_girl:{keywords:["home","parent","people","human","children"],char:"👨‍👧‍👧",fitzpatrick_scale:false,category:"people"},yarn:{keywords:["ball","crochet","knit"],char:"🧶",fitzpatrick_scale:false,category:"people"},thread:{keywords:["needle","sewing","spool","string"],char:"🧵",fitzpatrick_scale:false,category:"people"},coat:{keywords:["jacket"],char:"🧥",fitzpatrick_scale:false,category:"people"},labcoat:{keywords:["doctor","experiment","scientist","chemist"],char:"🥼",fitzpatrick_scale:false,category:"people"},womans_clothes:{keywords:["fashion","shopping_bags","female"],char:"👚",fitzpatrick_scale:false,category:"people"},tshirt:{keywords:["fashion","cloth","casual","shirt","tee"],char:"👕",fitzpatrick_scale:false,category:"people"},jeans:{keywords:["fashion","shopping"],char:"👖",fitzpatrick_scale:false,category:"people"},necktie:{keywords:["shirt","suitup","formal","fashion","cloth","business"],char:"👔",fitzpatrick_scale:false,category:"people"},dress:{keywords:["clothes","fashion","shopping"],char:"👗",fitzpatrick_scale:false,category:"people"},bikini:{keywords:["swimming","female","woman","girl","fashion","beach","summer"],char:"👙",fitzpatrick_scale:false,category:"people"},kimono:{keywords:["dress","fashion","women","female","japanese"],char:"👘",fitzpatrick_scale:false,category:"people"},lipstick:{keywords:["female","girl","fashion","woman"],char:"💄",fitzpatrick_scale:false,category:"people"},kiss:{keywords:["face","lips","love","like","affection","valentines"],char:"💋",fitzpatrick_scale:false,category:"people"},footprints:{keywords:["feet","tracking","walking","beach"],char:"👣",fitzpatrick_scale:false,category:"people"},flat_shoe:{keywords:["ballet","slip-on","slipper"],char:"🥿",fitzpatrick_scale:false,category:"people"},high_heel:{keywords:["fashion","shoes","female","pumps","stiletto"],char:"👠",fitzpatrick_scale:false,category:"people"},sandal:{keywords:["shoes","fashion","flip flops"],char:"👡",fitzpatrick_scale:false,category:"people"},boot:{keywords:["shoes","fashion"],char:"👢",fitzpatrick_scale:false,category:"people"},mans_shoe:{keywords:["fashion","male"],char:"👞",fitzpatrick_scale:false,category:"people"},athletic_shoe:{keywords:["shoes","sports","sneakers"],char:"👟",fitzpatrick_scale:false,category:"people"},hiking_boot:{keywords:["backpacking","camping","hiking"],char:"🥾",fitzpatrick_scale:false,category:"people"},socks:{keywords:["stockings","clothes"],char:"🧦",fitzpatrick_scale:false,category:"people"},gloves:{keywords:["hands","winter","clothes"],char:"🧤",fitzpatrick_scale:false,category:"people"},scarf:{keywords:["neck","winter","clothes"],char:"🧣",fitzpatrick_scale:false,category:"people"},womans_hat:{keywords:["fashion","accessories","female","lady","spring"],char:"👒",fitzpatrick_scale:false,category:"people"},tophat:{keywords:["magic","gentleman","classy","circus"],char:"🎩",fitzpatrick_scale:false,category:"people"},billed_hat:{keywords:["cap","baseball"],char:"🧢",fitzpatrick_scale:false,category:"people"},rescue_worker_helmet:{keywords:["construction","build"],char:"⛑",fitzpatrick_scale:false,category:"people"},mortar_board:{keywords:["school","college","degree","university","graduation","cap","hat","legal","learn","education"],char:"🎓",fitzpatrick_scale:false,category:"people"},crown:{keywords:["king","kod","leader","royalty","lord"],char:"👑",fitzpatrick_scale:false,category:"people"},school_satchel:{keywords:["student","education","bag","backpack"],char:"🎒",fitzpatrick_scale:false,category:"people"},luggage:{keywords:["packing","travel"],char:"🧳",fitzpatrick_scale:false,category:"people"},pouch:{keywords:["bag","accessories","shopping"],char:"👝",fitzpatrick_scale:false,category:"people"},purse:{keywords:["fashion","accessories","money","sales","shopping"],char:"👛",fitzpatrick_scale:false,category:"people"},handbag:{keywords:["fashion","accessory","accessories","shopping"],char:"👜",fitzpatrick_scale:false,category:"people"},briefcase:{keywords:["business","documents","work","law","legal","job","career"],char:"💼",fitzpatrick_scale:false,category:"people"},eyeglasses:{keywords:["fashion","accessories","eyesight","nerdy","dork","geek"],char:"👓",fitzpatrick_scale:false,category:"people"},dark_sunglasses:{keywords:["face","cool","accessories"],char:"🕶",fitzpatrick_scale:false,category:"people"},goggles:{keywords:["eyes","protection","safety"],char:"🥽",fitzpatrick_scale:false,category:"people"},ring:{keywords:["wedding","propose","marriage","valentines","diamond","fashion","jewelry","gem","engagement"],char:"💍",fitzpatrick_scale:false,category:"people"},closed_umbrella:{keywords:["weather","rain","drizzle"],char:"🌂",fitzpatrick_scale:false,category:"people"},dog:{keywords:["animal","friend","nature","woof","puppy","pet","faithful"],char:"🐶",fitzpatrick_scale:false,category:"animals_and_nature"},cat:{keywords:["animal","meow","nature","pet","kitten"],char:"🐱",fitzpatrick_scale:false,category:"animals_and_nature"},mouse:{keywords:["animal","nature","cheese_wedge","rodent"],char:"🐭",fitzpatrick_scale:false,category:"animals_and_nature"},hamster:{keywords:["animal","nature"],char:"🐹",fitzpatrick_scale:false,category:"animals_and_nature"},rabbit:{keywords:["animal","nature","pet","spring","magic","bunny"],char:"🐰",fitzpatrick_scale:false,category:"animals_and_nature"},fox_face:{keywords:["animal","nature","face"],char:"🦊",fitzpatrick_scale:false,category:"animals_and_nature"},bear:{keywords:["animal","nature","wild"],char:"🐻",fitzpatrick_scale:false,category:"animals_and_nature"},panda_face:{keywords:["animal","nature","panda"],char:"🐼",fitzpatrick_scale:false,category:"animals_and_nature"},koala:{keywords:["animal","nature"],char:"🐨",fitzpatrick_scale:false,category:"animals_and_nature"},tiger:{keywords:["animal","cat","danger","wild","nature","roar"],char:"🐯",fitzpatrick_scale:false,category:"animals_and_nature"},lion:{keywords:["animal","nature"],char:"🦁",fitzpatrick_scale:false,category:"animals_and_nature"},cow:{keywords:["beef","ox","animal","nature","moo","milk"],char:"🐮",fitzpatrick_scale:false,category:"animals_and_nature"},pig:{keywords:["animal","oink","nature"],char:"🐷",fitzpatrick_scale:false,category:"animals_and_nature"},pig_nose:{keywords:["animal","oink"],char:"🐽",fitzpatrick_scale:false,category:"animals_and_nature"},frog:{keywords:["animal","nature","croak","toad"],char:"🐸",fitzpatrick_scale:false,category:"animals_and_nature"},squid:{keywords:["animal","nature","ocean","sea"],char:"🦑",fitzpatrick_scale:false,category:"animals_and_nature"},octopus:{keywords:["animal","creature","ocean","sea","nature","beach"],char:"🐙",fitzpatrick_scale:false,category:"animals_and_nature"},shrimp:{keywords:["animal","ocean","nature","seafood"],char:"🦐",fitzpatrick_scale:false,category:"animals_and_nature"},monkey_face:{keywords:["animal","nature","circus"],char:"🐵",fitzpatrick_scale:false,category:"animals_and_nature"},gorilla:{keywords:["animal","nature","circus"],char:"🦍",fitzpatrick_scale:false,category:"animals_and_nature"},see_no_evil:{keywords:["monkey","animal","nature","haha"],char:"🙈",fitzpatrick_scale:false,category:"animals_and_nature"},hear_no_evil:{keywords:["animal","monkey","nature"],char:"🙉",fitzpatrick_scale:false,category:"animals_and_nature"},speak_no_evil:{keywords:["monkey","animal","nature","omg"],char:"🙊",fitzpatrick_scale:false,category:"animals_and_nature"},monkey:{keywords:["animal","nature","banana","circus"],char:"🐒",fitzpatrick_scale:false,category:"animals_and_nature"},chicken:{keywords:["animal","cluck","nature","bird"],char:"🐔",fitzpatrick_scale:false,category:"animals_and_nature"},penguin:{keywords:["animal","nature"],char:"🐧",fitzpatrick_scale:false,category:"animals_and_nature"},bird:{keywords:["animal","nature","fly","tweet","spring"],char:"🐦",fitzpatrick_scale:false,category:"animals_and_nature"},baby_chick:{keywords:["animal","chicken","bird"],char:"🐤",fitzpatrick_scale:false,category:"animals_and_nature"},hatching_chick:{keywords:["animal","chicken","egg","born","baby","bird"],char:"🐣",fitzpatrick_scale:false,category:"animals_and_nature"},hatched_chick:{keywords:["animal","chicken","baby","bird"],char:"🐥",fitzpatrick_scale:false,category:"animals_and_nature"},duck:{keywords:["animal","nature","bird","mallard"],char:"🦆",fitzpatrick_scale:false,category:"animals_and_nature"},eagle:{keywords:["animal","nature","bird"],char:"🦅",fitzpatrick_scale:false,category:"animals_and_nature"},owl:{keywords:["animal","nature","bird","hoot"],char:"🦉",fitzpatrick_scale:false,category:"animals_and_nature"},bat:{keywords:["animal","nature","blind","vampire"],char:"🦇",fitzpatrick_scale:false,category:"animals_and_nature"},wolf:{keywords:["animal","nature","wild"],char:"🐺",fitzpatrick_scale:false,category:"animals_and_nature"},boar:{keywords:["animal","nature"],char:"🐗",fitzpatrick_scale:false,category:"animals_and_nature"},horse:{keywords:["animal","brown","nature"],char:"🐴",fitzpatrick_scale:false,category:"animals_and_nature"},unicorn:{keywords:["animal","nature","mystical"],char:"🦄",fitzpatrick_scale:false,category:"animals_and_nature"},honeybee:{keywords:["animal","insect","nature","bug","spring","honey"],char:"🐝",fitzpatrick_scale:false,category:"animals_and_nature"},bug:{keywords:["animal","insect","nature","worm"],char:"🐛",fitzpatrick_scale:false,category:"animals_and_nature"},butterfly:{keywords:["animal","insect","nature","caterpillar"],char:"🦋",fitzpatrick_scale:false,category:"animals_and_nature"},snail:{keywords:["slow","animal","shell"],char:"🐌",fitzpatrick_scale:false,category:"animals_and_nature"},beetle:{keywords:["animal","insect","nature","ladybug"],char:"🐞",fitzpatrick_scale:false,category:"animals_and_nature"},ant:{keywords:["animal","insect","nature","bug"],char:"🐜",fitzpatrick_scale:false,category:"animals_and_nature"},grasshopper:{keywords:["animal","cricket","chirp"],char:"🦗",fitzpatrick_scale:false,category:"animals_and_nature"},spider:{keywords:["animal","arachnid"],char:"🕷",fitzpatrick_scale:false,category:"animals_and_nature"},scorpion:{keywords:["animal","arachnid"],char:"🦂",fitzpatrick_scale:false,category:"animals_and_nature"},crab:{keywords:["animal","crustacean"],char:"🦀",fitzpatrick_scale:false,category:"animals_and_nature"},snake:{keywords:["animal","evil","nature","hiss","python"],char:"🐍",fitzpatrick_scale:false,category:"animals_and_nature"},lizard:{keywords:["animal","nature","reptile"],char:"🦎",fitzpatrick_scale:false,category:"animals_and_nature"},"t-rex":{keywords:["animal","nature","dinosaur","tyrannosaurus","extinct"],char:"🦖",fitzpatrick_scale:false,category:"animals_and_nature"},sauropod:{keywords:["animal","nature","dinosaur","brachiosaurus","brontosaurus","diplodocus","extinct"],char:"🦕",fitzpatrick_scale:false,category:"animals_and_nature"},turtle:{keywords:["animal","slow","nature","tortoise"],char:"🐢",fitzpatrick_scale:false,category:"animals_and_nature"},tropical_fish:{keywords:["animal","swim","ocean","beach","nemo"],char:"🐠",fitzpatrick_scale:false,category:"animals_and_nature"},fish:{keywords:["animal","food","nature"],char:"🐟",fitzpatrick_scale:false,category:"animals_and_nature"},blowfish:{keywords:["animal","nature","food","sea","ocean"],char:"🐡",fitzpatrick_scale:false,category:"animals_and_nature"},dolphin:{keywords:["animal","nature","fish","sea","ocean","flipper","fins","beach"],char:"🐬",fitzpatrick_scale:false,category:"animals_and_nature"},shark:{keywords:["animal","nature","fish","sea","ocean","jaws","fins","beach"],char:"🦈",fitzpatrick_scale:false,category:"animals_and_nature"},whale:{keywords:["animal","nature","sea","ocean"],char:"🐳",fitzpatrick_scale:false,category:"animals_and_nature"},whale2:{keywords:["animal","nature","sea","ocean"],char:"🐋",fitzpatrick_scale:false,category:"animals_and_nature"},crocodile:{keywords:["animal","nature","reptile","lizard","alligator"],char:"🐊",fitzpatrick_scale:false,category:"animals_and_nature"},leopard:{keywords:["animal","nature"],char:"🐆",fitzpatrick_scale:false,category:"animals_and_nature"},zebra:{keywords:["animal","nature","stripes","safari"],char:"🦓",fitzpatrick_scale:false,category:"animals_and_nature"},tiger2:{keywords:["animal","nature","roar"],char:"🐅",fitzpatrick_scale:false,category:"animals_and_nature"},water_buffalo:{keywords:["animal","nature","ox","cow"],char:"🐃",fitzpatrick_scale:false,category:"animals_and_nature"},ox:{keywords:["animal","cow","beef"],char:"🐂",fitzpatrick_scale:false,category:"animals_and_nature"},cow2:{keywords:["beef","ox","animal","nature","moo","milk"],char:"🐄",fitzpatrick_scale:false,category:"animals_and_nature"},deer:{keywords:["animal","nature","horns","venison"],char:"🦌",fitzpatrick_scale:false,category:"animals_and_nature"},dromedary_camel:{keywords:["animal","hot","desert","hump"],char:"🐪",fitzpatrick_scale:false,category:"animals_and_nature"},camel:{keywords:["animal","nature","hot","desert","hump"],char:"🐫",fitzpatrick_scale:false,category:"animals_and_nature"},giraffe:{keywords:["animal","nature","spots","safari"],char:"🦒",fitzpatrick_scale:false,category:"animals_and_nature"},elephant:{keywords:["animal","nature","nose","th","circus"],char:"🐘",fitzpatrick_scale:false,category:"animals_and_nature"},rhinoceros:{keywords:["animal","nature","horn"],char:"🦏",fitzpatrick_scale:false,category:"animals_and_nature"},goat:{keywords:["animal","nature"],char:"🐐",fitzpatrick_scale:false,category:"animals_and_nature"},ram:{keywords:["animal","sheep","nature"],char:"🐏",fitzpatrick_scale:false,category:"animals_and_nature"},sheep:{keywords:["animal","nature","wool","shipit"],char:"🐑",fitzpatrick_scale:false,category:"animals_and_nature"},racehorse:{keywords:["animal","gamble","luck"],char:"🐎",fitzpatrick_scale:false,category:"animals_and_nature"},pig2:{keywords:["animal","nature"],char:"🐖",fitzpatrick_scale:false,category:"animals_and_nature"},rat:{keywords:["animal","mouse","rodent"],char:"🐀",fitzpatrick_scale:false,category:"animals_and_nature"},mouse2:{keywords:["animal","nature","rodent"],char:"🐁",fitzpatrick_scale:false,category:"animals_and_nature"},rooster:{keywords:["animal","nature","chicken"],char:"🐓",fitzpatrick_scale:false,category:"animals_and_nature"},turkey:{keywords:["animal","bird"],char:"🦃",fitzpatrick_scale:false,category:"animals_and_nature"},dove:{keywords:["animal","bird"],char:"🕊",fitzpatrick_scale:false,category:"animals_and_nature"},dog2:{keywords:["animal","nature","friend","doge","pet","faithful"],char:"🐕",fitzpatrick_scale:false,category:"animals_and_nature"},poodle:{keywords:["dog","animal","101","nature","pet"],char:"🐩",fitzpatrick_scale:false,category:"animals_and_nature"},cat2:{keywords:["animal","meow","pet","cats"],char:"🐈",fitzpatrick_scale:false,category:"animals_and_nature"},rabbit2:{keywords:["animal","nature","pet","magic","spring"],char:"🐇",fitzpatrick_scale:false,category:"animals_and_nature"},chipmunk:{keywords:["animal","nature","rodent","squirrel"],char:"🐿",fitzpatrick_scale:false,category:"animals_and_nature"},hedgehog:{keywords:["animal","nature","spiny"],char:"🦔",fitzpatrick_scale:false,category:"animals_and_nature"},raccoon:{keywords:["animal","nature"],char:"🦝",fitzpatrick_scale:false,category:"animals_and_nature"},llama:{keywords:["animal","nature","alpaca"],char:"🦙",fitzpatrick_scale:false,category:"animals_and_nature"},hippopotamus:{keywords:["animal","nature"],char:"🦛",fitzpatrick_scale:false,category:"animals_and_nature"},kangaroo:{keywords:["animal","nature","australia","joey","hop","marsupial"],char:"🦘",fitzpatrick_scale:false,category:"animals_and_nature"},badger:{keywords:["animal","nature","honey"],char:"🦡",fitzpatrick_scale:false,category:"animals_and_nature"},swan:{keywords:["animal","nature","bird"],char:"🦢",fitzpatrick_scale:false,category:"animals_and_nature"},peacock:{keywords:["animal","nature","peahen","bird"],char:"🦚",fitzpatrick_scale:false,category:"animals_and_nature"},parrot:{keywords:["animal","nature","bird","pirate","talk"],char:"🦜",fitzpatrick_scale:false,category:"animals_and_nature"},lobster:{keywords:["animal","nature","bisque","claws","seafood"],char:"🦞",fitzpatrick_scale:false,category:"animals_and_nature"},mosquito:{keywords:["animal","nature","insect","malaria"],char:"🦟",fitzpatrick_scale:false,category:"animals_and_nature"},paw_prints:{keywords:["animal","tracking","footprints","dog","cat","pet","feet"],char:"🐾",fitzpatrick_scale:false,category:"animals_and_nature"},dragon:{keywords:["animal","myth","nature","chinese","green"],char:"🐉",fitzpatrick_scale:false,category:"animals_and_nature"},dragon_face:{keywords:["animal","myth","nature","chinese","green"],char:"🐲",fitzpatrick_scale:false,category:"animals_and_nature"},cactus:{keywords:["vegetable","plant","nature"],char:"🌵",fitzpatrick_scale:false,category:"animals_and_nature"},christmas_tree:{keywords:["festival","vacation","december","xmas","celebration"],char:"🎄",fitzpatrick_scale:false,category:"animals_and_nature"},evergreen_tree:{keywords:["plant","nature"],char:"🌲",fitzpatrick_scale:false,category:"animals_and_nature"},deciduous_tree:{keywords:["plant","nature"],char:"🌳",fitzpatrick_scale:false,category:"animals_and_nature"},palm_tree:{keywords:["plant","vegetable","nature","summer","beach","mojito","tropical"],char:"🌴",fitzpatrick_scale:false,category:"animals_and_nature"},seedling:{keywords:["plant","nature","grass","lawn","spring"],char:"🌱",fitzpatrick_scale:false,category:"animals_and_nature"},herb:{keywords:["vegetable","plant","medicine","weed","grass","lawn"],char:"🌿",fitzpatrick_scale:false,category:"animals_and_nature"},shamrock:{keywords:["vegetable","plant","nature","irish","clover"],char:"☘",fitzpatrick_scale:false,category:"animals_and_nature"},four_leaf_clover:{keywords:["vegetable","plant","nature","lucky","irish"],char:"🍀",fitzpatrick_scale:false,category:"animals_and_nature"},bamboo:{keywords:["plant","nature","vegetable","panda","pine_decoration"],char:"🎍",fitzpatrick_scale:false,category:"animals_and_nature"},tanabata_tree:{keywords:["plant","nature","branch","summer"],char:"🎋",fitzpatrick_scale:false,category:"animals_and_nature"},leaves:{keywords:["nature","plant","tree","vegetable","grass","lawn","spring"],char:"🍃",fitzpatrick_scale:false,category:"animals_and_nature"},fallen_leaf:{keywords:["nature","plant","vegetable","leaves"],char:"🍂",fitzpatrick_scale:false,category:"animals_and_nature"},maple_leaf:{keywords:["nature","plant","vegetable","ca","fall"],char:"🍁",fitzpatrick_scale:false,category:"animals_and_nature"},ear_of_rice:{keywords:["nature","plant"],char:"🌾",fitzpatrick_scale:false,category:"animals_and_nature"},hibiscus:{keywords:["plant","vegetable","flowers","beach"],char:"🌺",fitzpatrick_scale:false,category:"animals_and_nature"},sunflower:{keywords:["nature","plant","fall"],char:"🌻",fitzpatrick_scale:false,category:"animals_and_nature"},rose:{keywords:["flowers","valentines","love","spring"],char:"🌹",fitzpatrick_scale:false,category:"animals_and_nature"},wilted_flower:{keywords:["plant","nature","flower"],char:"🥀",fitzpatrick_scale:false,category:"animals_and_nature"},tulip:{keywords:["flowers","plant","nature","summer","spring"],char:"🌷",fitzpatrick_scale:false,category:"animals_and_nature"},blossom:{keywords:["nature","flowers","yellow"],char:"🌼",fitzpatrick_scale:false,category:"animals_and_nature"},cherry_blossom:{keywords:["nature","plant","spring","flower"],char:"🌸",fitzpatrick_scale:false,category:"animals_and_nature"},bouquet:{keywords:["flowers","nature","spring"],char:"💐",fitzpatrick_scale:false,category:"animals_and_nature"},mushroom:{keywords:["plant","vegetable"],char:"🍄",fitzpatrick_scale:false,category:"animals_and_nature"},chestnut:{keywords:["food","squirrel"],char:"🌰",fitzpatrick_scale:false,category:"animals_and_nature"},jack_o_lantern:{keywords:["halloween","light","pumpkin","creepy","fall"],char:"🎃",fitzpatrick_scale:false,category:"animals_and_nature"},shell:{keywords:["nature","sea","beach"],char:"🐚",fitzpatrick_scale:false,category:"animals_and_nature"},spider_web:{keywords:["animal","insect","arachnid","silk"],char:"🕸",fitzpatrick_scale:false,category:"animals_and_nature"},earth_americas:{keywords:["globe","world","USA","international"],char:"🌎",fitzpatrick_scale:false,category:"animals_and_nature"},earth_africa:{keywords:["globe","world","international"],char:"🌍",fitzpatrick_scale:false,category:"animals_and_nature"},earth_asia:{keywords:["globe","world","east","international"],char:"🌏",fitzpatrick_scale:false,category:"animals_and_nature"},full_moon:{keywords:["nature","yellow","twilight","planet","space","night","evening","sleep"],char:"🌕",fitzpatrick_scale:false,category:"animals_and_nature"},waning_gibbous_moon:{keywords:["nature","twilight","planet","space","night","evening","sleep","waxing_gibbous_moon"],char:"🌖",fitzpatrick_scale:false,category:"animals_and_nature"},last_quarter_moon:{keywords:["nature","twilight","planet","space","night","evening","sleep"],char:"🌗",fitzpatrick_scale:false,category:"animals_and_nature"},waning_crescent_moon:{keywords:["nature","twilight","planet","space","night","evening","sleep"],char:"🌘",fitzpatrick_scale:false,category:"animals_and_nature"},new_moon:{keywords:["nature","twilight","planet","space","night","evening","sleep"],char:"🌑",fitzpatrick_scale:false,category:"animals_and_nature"},waxing_crescent_moon:{keywords:["nature","twilight","planet","space","night","evening","sleep"],char:"🌒",fitzpatrick_scale:false,category:"animals_and_nature"},first_quarter_moon:{keywords:["nature","twilight","planet","space","night","evening","sleep"],char:"🌓",fitzpatrick_scale:false,category:"animals_and_nature"},waxing_gibbous_moon:{keywords:["nature","night","sky","gray","twilight","planet","space","evening","sleep"],char:"🌔",fitzpatrick_scale:false,category:"animals_and_nature"},new_moon_with_face:{keywords:["nature","twilight","planet","space","night","evening","sleep"],char:"🌚",fitzpatrick_scale:false,category:"animals_and_nature"},full_moon_with_face:{keywords:["nature","twilight","planet","space","night","evening","sleep"],char:"🌝",fitzpatrick_scale:false,category:"animals_and_nature"},first_quarter_moon_with_face:{keywords:["nature","twilight","planet","space","night","evening","sleep"],char:"🌛",fitzpatrick_scale:false,category:"animals_and_nature"},last_quarter_moon_with_face:{keywords:["nature","twilight","planet","space","night","evening","sleep"],char:"🌜",fitzpatrick_scale:false,category:"animals_and_nature"},sun_with_face:{keywords:["nature","morning","sky"],char:"🌞",fitzpatrick_scale:false,category:"animals_and_nature"},crescent_moon:{keywords:["night","sleep","sky","evening","magic"],char:"🌙",fitzpatrick_scale:false,category:"animals_and_nature"},star:{keywords:["night","yellow"],char:"⭐",fitzpatrick_scale:false,category:"animals_and_nature"},star2:{keywords:["night","sparkle","awesome","good","magic"],char:"🌟",fitzpatrick_scale:false,category:"animals_and_nature"},dizzy:{keywords:["star","sparkle","shoot","magic"],char:"💫",fitzpatrick_scale:false,category:"animals_and_nature"},sparkles:{keywords:["stars","shine","shiny","cool","awesome","good","magic"],char:"✨",fitzpatrick_scale:false,category:"animals_and_nature"},comet:{keywords:["space"],char:"☄",fitzpatrick_scale:false,category:"animals_and_nature"},sunny:{keywords:["weather","nature","brightness","summer","beach","spring"],char:"☀️",fitzpatrick_scale:false,category:"animals_and_nature"},sun_behind_small_cloud:{keywords:["weather"],char:"🌤",fitzpatrick_scale:false,category:"animals_and_nature"},partly_sunny:{keywords:["weather","nature","cloudy","morning","fall","spring"],char:"⛅",fitzpatrick_scale:false,category:"animals_and_nature"},sun_behind_large_cloud:{keywords:["weather"],char:"🌥",fitzpatrick_scale:false,category:"animals_and_nature"},sun_behind_rain_cloud:{keywords:["weather"],char:"🌦",fitzpatrick_scale:false,category:"animals_and_nature"},cloud:{keywords:["weather","sky"],char:"☁️",fitzpatrick_scale:false,category:"animals_and_nature"},cloud_with_rain:{keywords:["weather"],char:"🌧",fitzpatrick_scale:false,category:"animals_and_nature"},cloud_with_lightning_and_rain:{keywords:["weather","lightning"],char:"⛈",fitzpatrick_scale:false,category:"animals_and_nature"},cloud_with_lightning:{keywords:["weather","thunder"],char:"🌩",fitzpatrick_scale:false,category:"animals_and_nature"},zap:{keywords:["thunder","weather","lightning bolt","fast"],char:"⚡",fitzpatrick_scale:false,category:"animals_and_nature"},fire:{keywords:["hot","cook","flame"],char:"🔥",fitzpatrick_scale:false,category:"animals_and_nature"},boom:{keywords:["bomb","explode","explosion","collision","blown"],char:"💥",fitzpatrick_scale:false,category:"animals_and_nature"},snowflake:{keywords:["winter","season","cold","weather","christmas","xmas"],char:"❄️",fitzpatrick_scale:false,category:"animals_and_nature"},cloud_with_snow:{keywords:["weather"],char:"🌨",fitzpatrick_scale:false,category:"animals_and_nature"},snowman:{keywords:["winter","season","cold","weather","christmas","xmas","frozen","without_snow"],char:"⛄",fitzpatrick_scale:false,category:"animals_and_nature"},snowman_with_snow:{keywords:["winter","season","cold","weather","christmas","xmas","frozen"],char:"☃",fitzpatrick_scale:false,category:"animals_and_nature"},wind_face:{keywords:["gust","air"],char:"🌬",fitzpatrick_scale:false,category:"animals_and_nature"},dash:{keywords:["wind","air","fast","shoo","fart","smoke","puff"],char:"💨",fitzpatrick_scale:false,category:"animals_and_nature"},tornado:{keywords:["weather","cyclone","twister"],char:"🌪",fitzpatrick_scale:false,category:"animals_and_nature"},fog:{keywords:["weather"],char:"🌫",fitzpatrick_scale:false,category:"animals_and_nature"},open_umbrella:{keywords:["weather","spring"],char:"☂",fitzpatrick_scale:false,category:"animals_and_nature"},umbrella:{keywords:["rainy","weather","spring"],char:"☔",fitzpatrick_scale:false,category:"animals_and_nature"},droplet:{keywords:["water","drip","faucet","spring"],char:"💧",fitzpatrick_scale:false,category:"animals_and_nature"},sweat_drops:{keywords:["water","drip","oops"],char:"💦",fitzpatrick_scale:false,category:"animals_and_nature"},ocean:{keywords:["sea","water","wave","nature","tsunami","disaster"],char:"🌊",fitzpatrick_scale:false,category:"animals_and_nature"},green_apple:{keywords:["fruit","nature"],char:"🍏",fitzpatrick_scale:false,category:"food_and_drink"},apple:{keywords:["fruit","mac","school"],char:"🍎",fitzpatrick_scale:false,category:"food_and_drink"},pear:{keywords:["fruit","nature","food"],char:"🍐",fitzpatrick_scale:false,category:"food_and_drink"},tangerine:{keywords:["food","fruit","nature","orange"],char:"🍊",fitzpatrick_scale:false,category:"food_and_drink"},lemon:{keywords:["fruit","nature"],char:"🍋",fitzpatrick_scale:false,category:"food_and_drink"},banana:{keywords:["fruit","food","monkey"],char:"🍌",fitzpatrick_scale:false,category:"food_and_drink"},watermelon:{keywords:["fruit","food","picnic","summer"],char:"🍉",fitzpatrick_scale:false,category:"food_and_drink"},grapes:{keywords:["fruit","food","wine"],char:"🍇",fitzpatrick_scale:false,category:"food_and_drink"},strawberry:{keywords:["fruit","food","nature"],char:"🍓",fitzpatrick_scale:false,category:"food_and_drink"},melon:{keywords:["fruit","nature","food"],char:"🍈",fitzpatrick_scale:false,category:"food_and_drink"},cherries:{keywords:["food","fruit"],char:"🍒",fitzpatrick_scale:false,category:"food_and_drink"},peach:{keywords:["fruit","nature","food"],char:"🍑",fitzpatrick_scale:false,category:"food_and_drink"},pineapple:{keywords:["fruit","nature","food"],char:"🍍",fitzpatrick_scale:false,category:"food_and_drink"},coconut:{keywords:["fruit","nature","food","palm"],char:"🥥",fitzpatrick_scale:false,category:"food_and_drink"},kiwi_fruit:{keywords:["fruit","food"],char:"🥝",fitzpatrick_scale:false,category:"food_and_drink"},mango:{keywords:["fruit","food","tropical"],char:"🥭",fitzpatrick_scale:false,category:"food_and_drink"},avocado:{keywords:["fruit","food"],char:"🥑",fitzpatrick_scale:false,category:"food_and_drink"},broccoli:{keywords:["fruit","food","vegetable"],char:"🥦",fitzpatrick_scale:false,category:"food_and_drink"},tomato:{keywords:["fruit","vegetable","nature","food"],char:"🍅",fitzpatrick_scale:false,category:"food_and_drink"},eggplant:{keywords:["vegetable","nature","food","aubergine"],char:"🍆",fitzpatrick_scale:false,category:"food_and_drink"},cucumber:{keywords:["fruit","food","pickle"],char:"🥒",fitzpatrick_scale:false,category:"food_and_drink"},carrot:{keywords:["vegetable","food","orange"],char:"🥕",fitzpatrick_scale:false,category:"food_and_drink"},hot_pepper:{keywords:["food","spicy","chilli","chili"],char:"🌶",fitzpatrick_scale:false,category:"food_and_drink"},potato:{keywords:["food","tuber","vegatable","starch"],char:"🥔",fitzpatrick_scale:false,category:"food_and_drink"},corn:{keywords:["food","vegetable","plant"],char:"🌽",fitzpatrick_scale:false,category:"food_and_drink"},leafy_greens:{keywords:["food","vegetable","plant","bok choy","cabbage","kale","lettuce"],char:"🥬",fitzpatrick_scale:false,category:"food_and_drink"},sweet_potato:{keywords:["food","nature"],char:"🍠",fitzpatrick_scale:false,category:"food_and_drink"},peanuts:{keywords:["food","nut"],char:"🥜",fitzpatrick_scale:false,category:"food_and_drink"},honey_pot:{keywords:["bees","sweet","kitchen"],char:"🍯",fitzpatrick_scale:false,category:"food_and_drink"},croissant:{keywords:["food","bread","french"],char:"🥐",fitzpatrick_scale:false,category:"food_and_drink"},bread:{keywords:["food","wheat","breakfast","toast"],char:"🍞",fitzpatrick_scale:false,category:"food_and_drink"},baguette_bread:{keywords:["food","bread","french"],char:"🥖",fitzpatrick_scale:false,category:"food_and_drink"},bagel:{keywords:["food","bread","bakery","schmear"],char:"🥯",fitzpatrick_scale:false,category:"food_and_drink"},pretzel:{keywords:["food","bread","twisted"],char:"🥨",fitzpatrick_scale:false,category:"food_and_drink"},cheese:{keywords:["food","chadder"],char:"🧀",fitzpatrick_scale:false,category:"food_and_drink"},egg:{keywords:["food","chicken","breakfast"],char:"🥚",fitzpatrick_scale:false,category:"food_and_drink"},bacon:{keywords:["food","breakfast","pork","pig","meat"],char:"🥓",fitzpatrick_scale:false,category:"food_and_drink"},steak:{keywords:["food","cow","meat","cut","chop","lambchop","porkchop"],char:"🥩",fitzpatrick_scale:false,category:"food_and_drink"},pancakes:{keywords:["food","breakfast","flapjacks","hotcakes"],char:"🥞",fitzpatrick_scale:false,category:"food_and_drink"},poultry_leg:{keywords:["food","meat","drumstick","bird","chicken","turkey"],char:"🍗",fitzpatrick_scale:false,category:"food_and_drink"},meat_on_bone:{keywords:["good","food","drumstick"],char:"🍖",fitzpatrick_scale:false,category:"food_and_drink"},bone:{keywords:["skeleton"],char:"🦴",fitzpatrick_scale:false,category:"food_and_drink"},fried_shrimp:{keywords:["food","animal","appetizer","summer"],char:"🍤",fitzpatrick_scale:false,category:"food_and_drink"},fried_egg:{keywords:["food","breakfast","kitchen","egg"],char:"🍳",fitzpatrick_scale:false,category:"food_and_drink"},hamburger:{keywords:["meat","fast food","beef","cheeseburger","mcdonalds","burger king"],char:"🍔",fitzpatrick_scale:false,category:"food_and_drink"},fries:{keywords:["chips","snack","fast food"],char:"🍟",fitzpatrick_scale:false,category:"food_and_drink"},stuffed_flatbread:{keywords:["food","flatbread","stuffed","gyro"],char:"🥙",fitzpatrick_scale:false,category:"food_and_drink"},hotdog:{keywords:["food","frankfurter"],char:"🌭",fitzpatrick_scale:false,category:"food_and_drink"},pizza:{keywords:["food","party"],char:"🍕",fitzpatrick_scale:false,category:"food_and_drink"},sandwich:{keywords:["food","lunch","bread"],char:"🥪",fitzpatrick_scale:false,category:"food_and_drink"},canned_food:{keywords:["food","soup"],char:"🥫",fitzpatrick_scale:false,category:"food_and_drink"},spaghetti:{keywords:["food","italian","noodle"],char:"🍝",fitzpatrick_scale:false,category:"food_and_drink"},taco:{keywords:["food","mexican"],char:"🌮",fitzpatrick_scale:false,category:"food_and_drink"},burrito:{keywords:["food","mexican"],char:"🌯",fitzpatrick_scale:false,category:"food_and_drink"},green_salad:{keywords:["food","healthy","lettuce"],char:"🥗",fitzpatrick_scale:false,category:"food_and_drink"},shallow_pan_of_food:{keywords:["food","cooking","casserole","paella"],char:"🥘",fitzpatrick_scale:false,category:"food_and_drink"},ramen:{keywords:["food","japanese","noodle","chopsticks"],char:"🍜",fitzpatrick_scale:false,category:"food_and_drink"},stew:{keywords:["food","meat","soup"],char:"🍲",fitzpatrick_scale:false,category:"food_and_drink"},fish_cake:{keywords:["food","japan","sea","beach","narutomaki","pink","swirl","kamaboko","surimi","ramen"],char:"🍥",fitzpatrick_scale:false,category:"food_and_drink"},fortune_cookie:{keywords:["food","prophecy"],char:"🥠",fitzpatrick_scale:false,category:"food_and_drink"},sushi:{keywords:["food","fish","japanese","rice"],char:"🍣",fitzpatrick_scale:false,category:"food_and_drink"},bento:{keywords:["food","japanese","box"],char:"🍱",fitzpatrick_scale:false,category:"food_and_drink"},curry:{keywords:["food","spicy","hot","indian"],char:"🍛",fitzpatrick_scale:false,category:"food_and_drink"},rice_ball:{keywords:["food","japanese"],char:"🍙",fitzpatrick_scale:false,category:"food_and_drink"},rice:{keywords:["food","china","asian"],char:"🍚",fitzpatrick_scale:false,category:"food_and_drink"},rice_cracker:{keywords:["food","japanese"],char:"🍘",fitzpatrick_scale:false,category:"food_and_drink"},oden:{keywords:["food","japanese"],char:"🍢",fitzpatrick_scale:false,category:"food_and_drink"},dango:{keywords:["food","dessert","sweet","japanese","barbecue","meat"],char:"🍡",fitzpatrick_scale:false,category:"food_and_drink"},shaved_ice:{keywords:["hot","dessert","summer"],char:"🍧",fitzpatrick_scale:false,category:"food_and_drink"},ice_cream:{keywords:["food","hot","dessert"],char:"🍨",fitzpatrick_scale:false,category:"food_and_drink"},icecream:{keywords:["food","hot","dessert","summer"],char:"🍦",fitzpatrick_scale:false,category:"food_and_drink"},pie:{keywords:["food","dessert","pastry"],char:"🥧",fitzpatrick_scale:false,category:"food_and_drink"},cake:{keywords:["food","dessert"],char:"🍰",fitzpatrick_scale:false,category:"food_and_drink"},cupcake:{keywords:["food","dessert","bakery","sweet"],char:"🧁",fitzpatrick_scale:false,category:"food_and_drink"},moon_cake:{keywords:["food","autumn"],char:"🥮",fitzpatrick_scale:false,category:"food_and_drink"},birthday:{keywords:["food","dessert","cake"],char:"🎂",fitzpatrick_scale:false,category:"food_and_drink"},custard:{keywords:["dessert","food"],char:"🍮",fitzpatrick_scale:false,category:"food_and_drink"},candy:{keywords:["snack","dessert","sweet","lolly"],char:"🍬",fitzpatrick_scale:false,category:"food_and_drink"},lollipop:{keywords:["food","snack","candy","sweet"],char:"🍭",fitzpatrick_scale:false,category:"food_and_drink"},chocolate_bar:{keywords:["food","snack","dessert","sweet"],char:"🍫",fitzpatrick_scale:false,category:"food_and_drink"},popcorn:{keywords:["food","movie theater","films","snack"],char:"🍿",fitzpatrick_scale:false,category:"food_and_drink"},dumpling:{keywords:["food","empanada","pierogi","potsticker"],char:"🥟",fitzpatrick_scale:false,category:"food_and_drink"},doughnut:{keywords:["food","dessert","snack","sweet","donut"],char:"🍩",fitzpatrick_scale:false,category:"food_and_drink"},cookie:{keywords:["food","snack","oreo","chocolate","sweet","dessert"],char:"🍪",fitzpatrick_scale:false,category:"food_and_drink"},milk_glass:{keywords:["beverage","drink","cow"],char:"🥛",fitzpatrick_scale:false,category:"food_and_drink"},beer:{keywords:["relax","beverage","drink","drunk","party","pub","summer","alcohol","booze"],char:"🍺",fitzpatrick_scale:false,category:"food_and_drink"},beers:{keywords:["relax","beverage","drink","drunk","party","pub","summer","alcohol","booze"],char:"🍻",fitzpatrick_scale:false,category:"food_and_drink"},clinking_glasses:{keywords:["beverage","drink","party","alcohol","celebrate","cheers","wine","champagne","toast"],char:"🥂",fitzpatrick_scale:false,category:"food_and_drink"},wine_glass:{keywords:["drink","beverage","drunk","alcohol","booze"],char:"🍷",fitzpatrick_scale:false,category:"food_and_drink"},tumbler_glass:{keywords:["drink","beverage","drunk","alcohol","liquor","booze","bourbon","scotch","whisky","glass","shot"],char:"🥃",fitzpatrick_scale:false,category:"food_and_drink"},cocktail:{keywords:["drink","drunk","alcohol","beverage","booze","mojito"],char:"🍸",fitzpatrick_scale:false,category:"food_and_drink"},tropical_drink:{keywords:["beverage","cocktail","summer","beach","alcohol","booze","mojito"],char:"🍹",fitzpatrick_scale:false,category:"food_and_drink"},champagne:{keywords:["drink","wine","bottle","celebration"],char:"🍾",fitzpatrick_scale:false,category:"food_and_drink"},sake:{keywords:["wine","drink","drunk","beverage","japanese","alcohol","booze"],char:"🍶",fitzpatrick_scale:false,category:"food_and_drink"},tea:{keywords:["drink","bowl","breakfast","green","british"],char:"🍵",fitzpatrick_scale:false,category:"food_and_drink"},cup_with_straw:{keywords:["drink","soda"],char:"🥤",fitzpatrick_scale:false,category:"food_and_drink"},coffee:{keywords:["beverage","caffeine","latte","espresso"],char:"☕",fitzpatrick_scale:false,category:"food_and_drink"},baby_bottle:{keywords:["food","container","milk"],char:"🍼",fitzpatrick_scale:false,category:"food_and_drink"},salt:{keywords:["condiment","shaker"],char:"🧂",fitzpatrick_scale:false,category:"food_and_drink"},spoon:{keywords:["cutlery","kitchen","tableware"],char:"🥄",fitzpatrick_scale:false,category:"food_and_drink"},fork_and_knife:{keywords:["cutlery","kitchen"],char:"🍴",fitzpatrick_scale:false,category:"food_and_drink"},plate_with_cutlery:{keywords:["food","eat","meal","lunch","dinner","restaurant"],char:"🍽",fitzpatrick_scale:false,category:"food_and_drink"},bowl_with_spoon:{keywords:["food","breakfast","cereal","oatmeal","porridge"],char:"🥣",fitzpatrick_scale:false,category:"food_and_drink"},takeout_box:{keywords:["food","leftovers"],char:"🥡",fitzpatrick_scale:false,category:"food_and_drink"},chopsticks:{keywords:["food"],char:"🥢",fitzpatrick_scale:false,category:"food_and_drink"},soccer:{keywords:["sports","football"],char:"⚽",fitzpatrick_scale:false,category:"activity"},basketball:{keywords:["sports","balls","NBA"],char:"🏀",fitzpatrick_scale:false,category:"activity"},football:{keywords:["sports","balls","NFL"],char:"🏈",fitzpatrick_scale:false,category:"activity"},baseball:{keywords:["sports","balls"],char:"⚾",fitzpatrick_scale:false,category:"activity"},softball:{keywords:["sports","balls"],char:"🥎",fitzpatrick_scale:false,category:"activity"},tennis:{keywords:["sports","balls","green"],char:"🎾",fitzpatrick_scale:false,category:"activity"},volleyball:{keywords:["sports","balls"],char:"🏐",fitzpatrick_scale:false,category:"activity"},rugby_football:{keywords:["sports","team"],char:"🏉",fitzpatrick_scale:false,category:"activity"},flying_disc:{keywords:["sports","frisbee","ultimate"],char:"🥏",fitzpatrick_scale:false,category:"activity"},"8ball":{keywords:["pool","hobby","game","luck","magic"],char:"🎱",fitzpatrick_scale:false,category:"activity"},golf:{keywords:["sports","business","flag","hole","summer"],char:"⛳",fitzpatrick_scale:false,category:"activity"},golfing_woman:{keywords:["sports","business","woman","female"],char:"🏌️‍♀️",fitzpatrick_scale:false,category:"activity"},golfing_man:{keywords:["sports","business"],char:"🏌",fitzpatrick_scale:true,category:"activity"},ping_pong:{keywords:["sports","pingpong"],char:"🏓",fitzpatrick_scale:false,category:"activity"},badminton:{keywords:["sports"],char:"🏸",fitzpatrick_scale:false,category:"activity"},goal_net:{keywords:["sports"],char:"🥅",fitzpatrick_scale:false,category:"activity"},ice_hockey:{keywords:["sports"],char:"🏒",fitzpatrick_scale:false,category:"activity"},field_hockey:{keywords:["sports"],char:"🏑",fitzpatrick_scale:false,category:"activity"},lacrosse:{keywords:["sports","ball","stick"],char:"🥍",fitzpatrick_scale:false,category:"activity"},cricket:{keywords:["sports"],char:"🏏",fitzpatrick_scale:false,category:"activity"},ski:{keywords:["sports","winter","cold","snow"],char:"🎿",fitzpatrick_scale:false,category:"activity"},skier:{keywords:["sports","winter","snow"],char:"⛷",fitzpatrick_scale:false,category:"activity"},snowboarder:{keywords:["sports","winter"],char:"🏂",fitzpatrick_scale:true,category:"activity"},person_fencing:{keywords:["sports","fencing","sword"],char:"🤺",fitzpatrick_scale:false,category:"activity"},women_wrestling:{keywords:["sports","wrestlers"],char:"🤼‍♀️",fitzpatrick_scale:false,category:"activity"},men_wrestling:{keywords:["sports","wrestlers"],char:"🤼‍♂️",fitzpatrick_scale:false,category:"activity"},woman_cartwheeling:{keywords:["gymnastics"],char:"🤸‍♀️",fitzpatrick_scale:true,category:"activity"},man_cartwheeling:{keywords:["gymnastics"],char:"🤸‍♂️",fitzpatrick_scale:true,category:"activity"},woman_playing_handball:{keywords:["sports"],char:"🤾‍♀️",fitzpatrick_scale:true,category:"activity"},man_playing_handball:{keywords:["sports"],char:"🤾‍♂️",fitzpatrick_scale:true,category:"activity"},ice_skate:{keywords:["sports"],char:"⛸",fitzpatrick_scale:false,category:"activity"},curling_stone:{keywords:["sports"],char:"🥌",fitzpatrick_scale:false,category:"activity"},skateboard:{keywords:["board"],char:"🛹",fitzpatrick_scale:false,category:"activity"},sled:{keywords:["sleigh","luge","toboggan"],char:"🛷",fitzpatrick_scale:false,category:"activity"},bow_and_arrow:{keywords:["sports"],char:"🏹",fitzpatrick_scale:false,category:"activity"},fishing_pole_and_fish:{keywords:["food","hobby","summer"],char:"🎣",fitzpatrick_scale:false,category:"activity"},boxing_glove:{keywords:["sports","fighting"],char:"🥊",fitzpatrick_scale:false,category:"activity"},martial_arts_uniform:{keywords:["judo","karate","taekwondo"],char:"🥋",fitzpatrick_scale:false,category:"activity"},rowing_woman:{keywords:["sports","hobby","water","ship","woman","female"],char:"🚣‍♀️",fitzpatrick_scale:true,category:"activity"},rowing_man:{keywords:["sports","hobby","water","ship"],char:"🚣",fitzpatrick_scale:true,category:"activity"},climbing_woman:{keywords:["sports","hobby","woman","female","rock"],char:"🧗‍♀️",fitzpatrick_scale:true,category:"activity"},climbing_man:{keywords:["sports","hobby","man","male","rock"],char:"🧗‍♂️",fitzpatrick_scale:true,category:"activity"},swimming_woman:{keywords:["sports","exercise","human","athlete","water","summer","woman","female"],char:"🏊‍♀️",fitzpatrick_scale:true,category:"activity"},swimming_man:{keywords:["sports","exercise","human","athlete","water","summer"],char:"🏊",fitzpatrick_scale:true,category:"activity"},woman_playing_water_polo:{keywords:["sports","pool"],char:"🤽‍♀️",fitzpatrick_scale:true,category:"activity"},man_playing_water_polo:{keywords:["sports","pool"],char:"🤽‍♂️",fitzpatrick_scale:true,category:"activity"},woman_in_lotus_position:{keywords:["woman","female","meditation","yoga","serenity","zen","mindfulness"],char:"🧘‍♀️",fitzpatrick_scale:true,category:"activity"},man_in_lotus_position:{keywords:["man","male","meditation","yoga","serenity","zen","mindfulness"],char:"🧘‍♂️",fitzpatrick_scale:true,category:"activity"},surfing_woman:{keywords:["sports","ocean","sea","summer","beach","woman","female"],char:"🏄‍♀️",fitzpatrick_scale:true,category:"activity"},surfing_man:{keywords:["sports","ocean","sea","summer","beach"],char:"🏄",fitzpatrick_scale:true,category:"activity"},bath:{keywords:["clean","shower","bathroom"],char:"🛀",fitzpatrick_scale:true,category:"activity"},basketball_woman:{keywords:["sports","human","woman","female"],char:"⛹️‍♀️",fitzpatrick_scale:true,category:"activity"},basketball_man:{keywords:["sports","human"],char:"⛹",fitzpatrick_scale:true,category:"activity"},weight_lifting_woman:{keywords:["sports","training","exercise","woman","female"],char:"🏋️‍♀️",fitzpatrick_scale:true,category:"activity"},weight_lifting_man:{keywords:["sports","training","exercise"],char:"🏋",fitzpatrick_scale:true,category:"activity"},biking_woman:{keywords:["sports","bike","exercise","hipster","woman","female"],char:"🚴‍♀️",fitzpatrick_scale:true,category:"activity"},biking_man:{keywords:["sports","bike","exercise","hipster"],char:"🚴",fitzpatrick_scale:true,category:"activity"},mountain_biking_woman:{keywords:["transportation","sports","human","race","bike","woman","female"],char:"🚵‍♀️",fitzpatrick_scale:true,category:"activity"},mountain_biking_man:{keywords:["transportation","sports","human","race","bike"],char:"🚵",fitzpatrick_scale:true,category:"activity"},horse_racing:{keywords:["animal","betting","competition","gambling","luck"],char:"🏇",fitzpatrick_scale:true,category:"activity"},business_suit_levitating:{keywords:["suit","business","levitate","hover","jump"],char:"🕴",fitzpatrick_scale:true,category:"activity"},trophy:{keywords:["win","award","contest","place","ftw","ceremony"],char:"🏆",fitzpatrick_scale:false,category:"activity"},running_shirt_with_sash:{keywords:["play","pageant"],char:"🎽",fitzpatrick_scale:false,category:"activity"},medal_sports:{keywords:["award","winning"],char:"🏅",fitzpatrick_scale:false,category:"activity"},medal_military:{keywords:["award","winning","army"],char:"🎖",fitzpatrick_scale:false,category:"activity"},"1st_place_medal":{keywords:["award","winning","first"],char:"🥇",fitzpatrick_scale:false,category:"activity"},"2nd_place_medal":{keywords:["award","second"],char:"🥈",fitzpatrick_scale:false,category:"activity"},"3rd_place_medal":{keywords:["award","third"],char:"🥉",fitzpatrick_scale:false,category:"activity"},reminder_ribbon:{keywords:["sports","cause","support","awareness"],char:"🎗",fitzpatrick_scale:false,category:"activity"},rosette:{keywords:["flower","decoration","military"],char:"🏵",fitzpatrick_scale:false,category:"activity"},ticket:{keywords:["event","concert","pass"],char:"🎫",fitzpatrick_scale:false,category:"activity"},tickets:{keywords:["sports","concert","entrance"],char:"🎟",fitzpatrick_scale:false,category:"activity"},performing_arts:{keywords:["acting","theater","drama"],char:"🎭",fitzpatrick_scale:false,category:"activity"},art:{keywords:["design","paint","draw","colors"],char:"🎨",fitzpatrick_scale:false,category:"activity"},circus_tent:{keywords:["festival","carnival","party"],char:"🎪",fitzpatrick_scale:false,category:"activity"},woman_juggling:{keywords:["juggle","balance","skill","multitask"],char:"🤹‍♀️",fitzpatrick_scale:true,category:"activity"},man_juggling:{keywords:["juggle","balance","skill","multitask"],char:"🤹‍♂️",fitzpatrick_scale:true,category:"activity"},microphone:{keywords:["sound","music","PA","sing","talkshow"],char:"🎤",fitzpatrick_scale:false,category:"activity"},headphones:{keywords:["music","score","gadgets"],char:"🎧",fitzpatrick_scale:false,category:"activity"},musical_score:{keywords:["treble","clef","compose"],char:"🎼",fitzpatrick_scale:false,category:"activity"},musical_keyboard:{keywords:["piano","instrument","compose"],char:"🎹",fitzpatrick_scale:false,category:"activity"},drum:{keywords:["music","instrument","drumsticks","snare"],char:"🥁",fitzpatrick_scale:false,category:"activity"},saxophone:{keywords:["music","instrument","jazz","blues"],char:"🎷",fitzpatrick_scale:false,category:"activity"},trumpet:{keywords:["music","brass"],char:"🎺",fitzpatrick_scale:false,category:"activity"},guitar:{keywords:["music","instrument"],char:"🎸",fitzpatrick_scale:false,category:"activity"},violin:{keywords:["music","instrument","orchestra","symphony"],char:"🎻",fitzpatrick_scale:false,category:"activity"},clapper:{keywords:["movie","film","record"],char:"🎬",fitzpatrick_scale:false,category:"activity"},video_game:{keywords:["play","console","PS4","controller"],char:"🎮",fitzpatrick_scale:false,category:"activity"},space_invader:{keywords:["game","arcade","play"],char:"👾",fitzpatrick_scale:false,category:"activity"},dart:{keywords:["game","play","bar","target","bullseye"],char:"🎯",fitzpatrick_scale:false,category:"activity"},game_die:{keywords:["dice","random","tabletop","play","luck"],char:"🎲",fitzpatrick_scale:false,category:"activity"},chess_pawn:{keywords:["expendable"],char:"♟",fitzpatrick_scale:false,category:"activity"},slot_machine:{keywords:["bet","gamble","vegas","fruit machine","luck","casino"],char:"🎰",fitzpatrick_scale:false,category:"activity"},jigsaw:{keywords:["interlocking","puzzle","piece"],char:"🧩",fitzpatrick_scale:false,category:"activity"},bowling:{keywords:["sports","fun","play"],char:"🎳",fitzpatrick_scale:false,category:"activity"},red_car:{keywords:["red","transportation","vehicle"],char:"🚗",fitzpatrick_scale:false,category:"travel_and_places"},taxi:{keywords:["uber","vehicle","cars","transportation"],char:"🚕",fitzpatrick_scale:false,category:"travel_and_places"},blue_car:{keywords:["transportation","vehicle"],char:"🚙",fitzpatrick_scale:false,category:"travel_and_places"},bus:{keywords:["car","vehicle","transportation"],char:"🚌",fitzpatrick_scale:false,category:"travel_and_places"},trolleybus:{keywords:["bart","transportation","vehicle"],char:"🚎",fitzpatrick_scale:false,category:"travel_and_places"},racing_car:{keywords:["sports","race","fast","formula","f1"],char:"🏎",fitzpatrick_scale:false,category:"travel_and_places"},police_car:{keywords:["vehicle","cars","transportation","law","legal","enforcement"],char:"🚓",fitzpatrick_scale:false,category:"travel_and_places"},ambulance:{keywords:["health","911","hospital"],char:"🚑",fitzpatrick_scale:false,category:"travel_and_places"},fire_engine:{keywords:["transportation","cars","vehicle"],char:"🚒",fitzpatrick_scale:false,category:"travel_and_places"},minibus:{keywords:["vehicle","car","transportation"],char:"🚐",fitzpatrick_scale:false,category:"travel_and_places"},truck:{keywords:["cars","transportation"],char:"🚚",fitzpatrick_scale:false,category:"travel_and_places"},articulated_lorry:{keywords:["vehicle","cars","transportation","express"],char:"🚛",fitzpatrick_scale:false,category:"travel_and_places"},tractor:{keywords:["vehicle","car","farming","agriculture"],char:"🚜",fitzpatrick_scale:false,category:"travel_and_places"},kick_scooter:{keywords:["vehicle","kick","razor"],char:"🛴",fitzpatrick_scale:false,category:"travel_and_places"},motorcycle:{keywords:["race","sports","fast"],char:"🏍",fitzpatrick_scale:false,category:"travel_and_places"},bike:{keywords:["sports","bicycle","exercise","hipster"],char:"🚲",fitzpatrick_scale:false,category:"travel_and_places"},motor_scooter:{keywords:["vehicle","vespa","sasha"],char:"🛵",fitzpatrick_scale:false,category:"travel_and_places"},rotating_light:{keywords:["police","ambulance","911","emergency","alert","error","pinged","law","legal"],char:"🚨",fitzpatrick_scale:false,category:"travel_and_places"},oncoming_police_car:{keywords:["vehicle","law","legal","enforcement","911"],char:"🚔",fitzpatrick_scale:false,category:"travel_and_places"},oncoming_bus:{keywords:["vehicle","transportation"],char:"🚍",fitzpatrick_scale:false,category:"travel_and_places"},oncoming_automobile:{keywords:["car","vehicle","transportation"],char:"🚘",fitzpatrick_scale:false,category:"travel_and_places"},oncoming_taxi:{keywords:["vehicle","cars","uber"],char:"🚖",fitzpatrick_scale:false,category:"travel_and_places"},aerial_tramway:{keywords:["transportation","vehicle","ski"],char:"🚡",fitzpatrick_scale:false,category:"travel_and_places"},mountain_cableway:{keywords:["transportation","vehicle","ski"],char:"🚠",fitzpatrick_scale:false,category:"travel_and_places"},suspension_railway:{keywords:["vehicle","transportation"],char:"🚟",fitzpatrick_scale:false,category:"travel_and_places"},railway_car:{keywords:["transportation","vehicle"],char:"🚃",fitzpatrick_scale:false,category:"travel_and_places"},train:{keywords:["transportation","vehicle","carriage","public","travel"],char:"🚋",fitzpatrick_scale:false,category:"travel_and_places"},monorail:{keywords:["transportation","vehicle"],char:"🚝",fitzpatrick_scale:false,category:"travel_and_places"},bullettrain_side:{keywords:["transportation","vehicle"],char:"🚄",fitzpatrick_scale:false,category:"travel_and_places"},bullettrain_front:{keywords:["transportation","vehicle","speed","fast","public","travel"],char:"🚅",fitzpatrick_scale:false,category:"travel_and_places"},light_rail:{keywords:["transportation","vehicle"],char:"🚈",fitzpatrick_scale:false,category:"travel_and_places"},mountain_railway:{keywords:["transportation","vehicle"],char:"🚞",fitzpatrick_scale:false,category:"travel_and_places"},steam_locomotive:{keywords:["transportation","vehicle","train"],char:"🚂",fitzpatrick_scale:false,category:"travel_and_places"},train2:{keywords:["transportation","vehicle"],char:"🚆",fitzpatrick_scale:false,category:"travel_and_places"},metro:{keywords:["transportation","blue-square","mrt","underground","tube"],char:"🚇",fitzpatrick_scale:false,category:"travel_and_places"},tram:{keywords:["transportation","vehicle"],char:"🚊",fitzpatrick_scale:false,category:"travel_and_places"},station:{keywords:["transportation","vehicle","public"],char:"🚉",fitzpatrick_scale:false,category:"travel_and_places"},flying_saucer:{keywords:["transportation","vehicle","ufo"],char:"🛸",fitzpatrick_scale:false,category:"travel_and_places"},helicopter:{keywords:["transportation","vehicle","fly"],char:"🚁",fitzpatrick_scale:false,category:"travel_and_places"},small_airplane:{keywords:["flight","transportation","fly","vehicle"],char:"🛩",fitzpatrick_scale:false,category:"travel_and_places"},airplane:{keywords:["vehicle","transportation","flight","fly"],char:"✈️",fitzpatrick_scale:false,category:"travel_and_places"},flight_departure:{keywords:["airport","flight","landing"],char:"🛫",fitzpatrick_scale:false,category:"travel_and_places"},flight_arrival:{keywords:["airport","flight","boarding"],char:"🛬",fitzpatrick_scale:false,category:"travel_and_places"},sailboat:{keywords:["ship","summer","transportation","water","sailing"],char:"⛵",fitzpatrick_scale:false,category:"travel_and_places"},motor_boat:{keywords:["ship"],char:"🛥",fitzpatrick_scale:false,category:"travel_and_places"},speedboat:{keywords:["ship","transportation","vehicle","summer"],char:"🚤",fitzpatrick_scale:false,category:"travel_and_places"},ferry:{keywords:["boat","ship","yacht"],char:"⛴",fitzpatrick_scale:false,category:"travel_and_places"},passenger_ship:{keywords:["yacht","cruise","ferry"],char:"🛳",fitzpatrick_scale:false,category:"travel_and_places"},rocket:{keywords:["launch","ship","staffmode","NASA","outer space","outer_space","fly"],char:"🚀",fitzpatrick_scale:false,category:"travel_and_places"},artificial_satellite:{keywords:["communication","gps","orbit","spaceflight","NASA","ISS"],char:"🛰",fitzpatrick_scale:false,category:"travel_and_places"},seat:{keywords:["sit","airplane","transport","bus","flight","fly"],char:"💺",fitzpatrick_scale:false,category:"travel_and_places"},canoe:{keywords:["boat","paddle","water","ship"],char:"🛶",fitzpatrick_scale:false,category:"travel_and_places"},anchor:{keywords:["ship","ferry","sea","boat"],char:"⚓",fitzpatrick_scale:false,category:"travel_and_places"},construction:{keywords:["wip","progress","caution","warning"],char:"🚧",fitzpatrick_scale:false,category:"travel_and_places"},fuelpump:{keywords:["gas station","petroleum"],char:"⛽",fitzpatrick_scale:false,category:"travel_and_places"},busstop:{keywords:["transportation","wait"],char:"🚏",fitzpatrick_scale:false,category:"travel_and_places"},vertical_traffic_light:{keywords:["transportation","driving"],char:"🚦",fitzpatrick_scale:false,category:"travel_and_places"},traffic_light:{keywords:["transportation","signal"],char:"🚥",fitzpatrick_scale:false,category:"travel_and_places"},checkered_flag:{keywords:["contest","finishline","race","gokart"],char:"🏁",fitzpatrick_scale:false,category:"travel_and_places"},ship:{keywords:["transportation","titanic","deploy"],char:"🚢",fitzpatrick_scale:false,category:"travel_and_places"},ferris_wheel:{keywords:["photo","carnival","londoneye"],char:"🎡",fitzpatrick_scale:false,category:"travel_and_places"},roller_coaster:{keywords:["carnival","playground","photo","fun"],char:"🎢",fitzpatrick_scale:false,category:"travel_and_places"},carousel_horse:{keywords:["photo","carnival"],char:"🎠",fitzpatrick_scale:false,category:"travel_and_places"},building_construction:{keywords:["wip","working","progress"],char:"🏗",fitzpatrick_scale:false,category:"travel_and_places"},foggy:{keywords:["photo","mountain"],char:"🌁",fitzpatrick_scale:false,category:"travel_and_places"},tokyo_tower:{keywords:["photo","japanese"],char:"🗼",fitzpatrick_scale:false,category:"travel_and_places"},factory:{keywords:["building","industry","pollution","smoke"],char:"🏭",fitzpatrick_scale:false,category:"travel_and_places"},fountain:{keywords:["photo","summer","water","fresh"],char:"⛲",fitzpatrick_scale:false,category:"travel_and_places"},rice_scene:{keywords:["photo","japan","asia","tsukimi"],char:"🎑",fitzpatrick_scale:false,category:"travel_and_places"},mountain:{keywords:["photo","nature","environment"],char:"⛰",fitzpatrick_scale:false,category:"travel_and_places"},mountain_snow:{keywords:["photo","nature","environment","winter","cold"],char:"🏔",fitzpatrick_scale:false,category:"travel_and_places"},mount_fuji:{keywords:["photo","mountain","nature","japanese"],char:"🗻",fitzpatrick_scale:false,category:"travel_and_places"},volcano:{keywords:["photo","nature","disaster"],char:"🌋",fitzpatrick_scale:false,category:"travel_and_places"},japan:{keywords:["nation","country","japanese","asia"],char:"🗾",fitzpatrick_scale:false,category:"travel_and_places"},camping:{keywords:["photo","outdoors","tent"],char:"🏕",fitzpatrick_scale:false,category:"travel_and_places"},tent:{keywords:["photo","camping","outdoors"],char:"⛺",fitzpatrick_scale:false,category:"travel_and_places"},national_park:{keywords:["photo","environment","nature"],char:"🏞",fitzpatrick_scale:false,category:"travel_and_places"},motorway:{keywords:["road","cupertino","interstate","highway"],char:"🛣",fitzpatrick_scale:false,category:"travel_and_places"},railway_track:{keywords:["train","transportation"],char:"🛤",fitzpatrick_scale:false,category:"travel_and_places"},sunrise:{keywords:["morning","view","vacation","photo"],char:"🌅",fitzpatrick_scale:false,category:"travel_and_places"},sunrise_over_mountains:{keywords:["view","vacation","photo"],char:"🌄",fitzpatrick_scale:false,category:"travel_and_places"},desert:{keywords:["photo","warm","saharah"],char:"🏜",fitzpatrick_scale:false,category:"travel_and_places"},beach_umbrella:{keywords:["weather","summer","sunny","sand","mojito"],char:"🏖",fitzpatrick_scale:false,category:"travel_and_places"},desert_island:{keywords:["photo","tropical","mojito"],char:"🏝",fitzpatrick_scale:false,category:"travel_and_places"},city_sunrise:{keywords:["photo","good morning","dawn"],char:"🌇",fitzpatrick_scale:false,category:"travel_and_places"},city_sunset:{keywords:["photo","evening","sky","buildings"],char:"🌆",fitzpatrick_scale:false,category:"travel_and_places"},cityscape:{keywords:["photo","night life","urban"],char:"🏙",fitzpatrick_scale:false,category:"travel_and_places"},night_with_stars:{keywords:["evening","city","downtown"],char:"🌃",fitzpatrick_scale:false,category:"travel_and_places"},bridge_at_night:{keywords:["photo","sanfrancisco"],char:"🌉",fitzpatrick_scale:false,category:"travel_and_places"},milky_way:{keywords:["photo","space","stars"],char:"🌌",fitzpatrick_scale:false,category:"travel_and_places"},stars:{keywords:["night","photo"],char:"🌠",fitzpatrick_scale:false,category:"travel_and_places"},sparkler:{keywords:["stars","night","shine"],char:"🎇",fitzpatrick_scale:false,category:"travel_and_places"},fireworks:{keywords:["photo","festival","carnival","congratulations"],char:"🎆",fitzpatrick_scale:false,category:"travel_and_places"},rainbow:{keywords:["nature","happy","unicorn_face","photo","sky","spring"],char:"🌈",fitzpatrick_scale:false,category:"travel_and_places"},houses:{keywords:["buildings","photo"],char:"🏘",fitzpatrick_scale:false,category:"travel_and_places"},european_castle:{keywords:["building","royalty","history"],char:"🏰",fitzpatrick_scale:false,category:"travel_and_places"},japanese_castle:{keywords:["photo","building"],char:"🏯",fitzpatrick_scale:false,category:"travel_and_places"},stadium:{keywords:["photo","place","sports","concert","venue"],char:"🏟",fitzpatrick_scale:false,category:"travel_and_places"},statue_of_liberty:{keywords:["american","newyork"],char:"🗽",fitzpatrick_scale:false,category:"travel_and_places"},house:{keywords:["building","home"],char:"🏠",fitzpatrick_scale:false,category:"travel_and_places"},house_with_garden:{keywords:["home","plant","nature"],char:"🏡",fitzpatrick_scale:false,category:"travel_and_places"},derelict_house:{keywords:["abandon","evict","broken","building"],char:"🏚",fitzpatrick_scale:false,category:"travel_and_places"},office:{keywords:["building","bureau","work"],char:"🏢",fitzpatrick_scale:false,category:"travel_and_places"},department_store:{keywords:["building","shopping","mall"],char:"🏬",fitzpatrick_scale:false,category:"travel_and_places"},post_office:{keywords:["building","envelope","communication"],char:"🏣",fitzpatrick_scale:false,category:"travel_and_places"},european_post_office:{keywords:["building","email"],char:"🏤",fitzpatrick_scale:false,category:"travel_and_places"},hospital:{keywords:["building","health","surgery","doctor"],char:"🏥",fitzpatrick_scale:false,category:"travel_and_places"},bank:{keywords:["building","money","sales","cash","business","enterprise"],char:"🏦",fitzpatrick_scale:false,category:"travel_and_places"},hotel:{keywords:["building","accomodation","checkin"],char:"🏨",fitzpatrick_scale:false,category:"travel_and_places"},convenience_store:{keywords:["building","shopping","groceries"],char:"🏪",fitzpatrick_scale:false,category:"travel_and_places"},school:{keywords:["building","student","education","learn","teach"],char:"🏫",fitzpatrick_scale:false,category:"travel_and_places"},love_hotel:{keywords:["like","affection","dating"],char:"🏩",fitzpatrick_scale:false,category:"travel_and_places"},wedding:{keywords:["love","like","affection","couple","marriage","bride","groom"],char:"💒",fitzpatrick_scale:false,category:"travel_and_places"},classical_building:{keywords:["art","culture","history"],char:"🏛",fitzpatrick_scale:false,category:"travel_and_places"},church:{keywords:["building","religion","christ"],char:"⛪",fitzpatrick_scale:false,category:"travel_and_places"},mosque:{keywords:["islam","worship","minaret"],char:"🕌",fitzpatrick_scale:false,category:"travel_and_places"},synagogue:{keywords:["judaism","worship","temple","jewish"],char:"🕍",fitzpatrick_scale:false,category:"travel_and_places"},kaaba:{keywords:["mecca","mosque","islam"],char:"🕋",fitzpatrick_scale:false,category:"travel_and_places"},shinto_shrine:{keywords:["temple","japan","kyoto"],char:"⛩",fitzpatrick_scale:false,category:"travel_and_places"},watch:{keywords:["time","accessories"],char:"⌚",fitzpatrick_scale:false,category:"objects"},iphone:{keywords:["technology","apple","gadgets","dial"],char:"📱",fitzpatrick_scale:false,category:"objects"},calling:{keywords:["iphone","incoming"],char:"📲",fitzpatrick_scale:false,category:"objects"},computer:{keywords:["technology","laptop","screen","display","monitor"],char:"💻",fitzpatrick_scale:false,category:"objects"},keyboard:{keywords:["technology","computer","type","input","text"],char:"⌨",fitzpatrick_scale:false,category:"objects"},desktop_computer:{keywords:["technology","computing","screen"],char:"🖥",fitzpatrick_scale:false,category:"objects"},printer:{keywords:["paper","ink"],char:"🖨",fitzpatrick_scale:false,category:"objects"},computer_mouse:{keywords:["click"],char:"🖱",fitzpatrick_scale:false,category:"objects"},trackball:{keywords:["technology","trackpad"],char:"🖲",fitzpatrick_scale:false,category:"objects"},joystick:{keywords:["game","play"],char:"🕹",fitzpatrick_scale:false,category:"objects"},clamp:{keywords:["tool"],char:"🗜",fitzpatrick_scale:false,category:"objects"},minidisc:{keywords:["technology","record","data","disk","90s"],char:"💽",fitzpatrick_scale:false,category:"objects"},floppy_disk:{keywords:["oldschool","technology","save","90s","80s"],char:"💾",fitzpatrick_scale:false,category:"objects"},cd:{keywords:["technology","dvd","disk","disc","90s"],char:"💿",fitzpatrick_scale:false,category:"objects"},dvd:{keywords:["cd","disk","disc"],char:"📀",fitzpatrick_scale:false,category:"objects"},vhs:{keywords:["record","video","oldschool","90s","80s"],char:"📼",fitzpatrick_scale:false,category:"objects"},camera:{keywords:["gadgets","photography"],char:"📷",fitzpatrick_scale:false,category:"objects"},camera_flash:{keywords:["photography","gadgets"],char:"📸",fitzpatrick_scale:false,category:"objects"},video_camera:{keywords:["film","record"],char:"📹",fitzpatrick_scale:false,category:"objects"},movie_camera:{keywords:["film","record"],char:"🎥",fitzpatrick_scale:false,category:"objects"},film_projector:{keywords:["video","tape","record","movie"],char:"📽",fitzpatrick_scale:false,category:"objects"},film_strip:{keywords:["movie"],char:"🎞",fitzpatrick_scale:false,category:"objects"},telephone_receiver:{keywords:["technology","communication","dial"],char:"📞",fitzpatrick_scale:false,category:"objects"},phone:{keywords:["technology","communication","dial","telephone"],char:"☎️",fitzpatrick_scale:false,category:"objects"},pager:{keywords:["bbcall","oldschool","90s"],char:"📟",fitzpatrick_scale:false,category:"objects"},fax:{keywords:["communication","technology"],char:"📠",fitzpatrick_scale:false,category:"objects"},tv:{keywords:["technology","program","oldschool","show","television"],char:"📺",fitzpatrick_scale:false,category:"objects"},radio:{keywords:["communication","music","podcast","program"],char:"📻",fitzpatrick_scale:false,category:"objects"},studio_microphone:{keywords:["sing","recording","artist","talkshow"],char:"🎙",fitzpatrick_scale:false,category:"objects"},level_slider:{keywords:["scale"],char:"🎚",fitzpatrick_scale:false,category:"objects"},control_knobs:{keywords:["dial"],char:"🎛",fitzpatrick_scale:false,category:"objects"},compass:{keywords:["magnetic","navigation","orienteering"],char:"🧭",fitzpatrick_scale:false,category:"objects"},stopwatch:{keywords:["time","deadline"],char:"⏱",fitzpatrick_scale:false,category:"objects"},timer_clock:{keywords:["alarm"],char:"⏲",fitzpatrick_scale:false,category:"objects"},alarm_clock:{keywords:["time","wake"],char:"⏰",fitzpatrick_scale:false,category:"objects"},mantelpiece_clock:{keywords:["time"],char:"🕰",fitzpatrick_scale:false,category:"objects"},hourglass_flowing_sand:{keywords:["oldschool","time","countdown"],char:"⏳",fitzpatrick_scale:false,category:"objects"},hourglass:{keywords:["time","clock","oldschool","limit","exam","quiz","test"],char:"⌛",fitzpatrick_scale:false,category:"objects"},satellite:{keywords:["communication","future","radio","space"],char:"📡",fitzpatrick_scale:false,category:"objects"},battery:{keywords:["power","energy","sustain"],char:"🔋",fitzpatrick_scale:false,category:"objects"},electric_plug:{keywords:["charger","power"],char:"🔌",fitzpatrick_scale:false,category:"objects"},bulb:{keywords:["light","electricity","idea"],char:"💡",fitzpatrick_scale:false,category:"objects"},flashlight:{keywords:["dark","camping","sight","night"],char:"🔦",fitzpatrick_scale:false,category:"objects"},candle:{keywords:["fire","wax"],char:"🕯",fitzpatrick_scale:false,category:"objects"},fire_extinguisher:{keywords:["quench"],char:"🧯",fitzpatrick_scale:false,category:"objects"},wastebasket:{keywords:["bin","trash","rubbish","garbage","toss"],char:"🗑",fitzpatrick_scale:false,category:"objects"},oil_drum:{keywords:["barrell"],char:"🛢",fitzpatrick_scale:false,category:"objects"},money_with_wings:{keywords:["dollar","bills","payment","sale"],char:"💸",fitzpatrick_scale:false,category:"objects"},dollar:{keywords:["money","sales","bill","currency"],char:"💵",fitzpatrick_scale:false,category:"objects"},yen:{keywords:["money","sales","japanese","dollar","currency"],char:"💴",fitzpatrick_scale:false,category:"objects"},euro:{keywords:["money","sales","dollar","currency"],char:"💶",fitzpatrick_scale:false,category:"objects"},pound:{keywords:["british","sterling","money","sales","bills","uk","england","currency"],char:"💷",fitzpatrick_scale:false,category:"objects"},moneybag:{keywords:["dollar","payment","coins","sale"],char:"💰",fitzpatrick_scale:false,category:"objects"},credit_card:{keywords:["money","sales","dollar","bill","payment","shopping"],char:"💳",fitzpatrick_scale:false,category:"objects"},gem:{keywords:["blue","ruby","diamond","jewelry"],char:"💎",fitzpatrick_scale:false,category:"objects"},balance_scale:{keywords:["law","fairness","weight"],char:"⚖",fitzpatrick_scale:false,category:"objects"},toolbox:{keywords:["tools","diy","fix","maintainer","mechanic"],char:"🧰",fitzpatrick_scale:false,category:"objects"},wrench:{keywords:["tools","diy","ikea","fix","maintainer"],char:"🔧",fitzpatrick_scale:false,category:"objects"},hammer:{keywords:["tools","build","create"],char:"🔨",fitzpatrick_scale:false,category:"objects"},hammer_and_pick:{keywords:["tools","build","create"],char:"⚒",fitzpatrick_scale:false,category:"objects"},hammer_and_wrench:{keywords:["tools","build","create"],char:"🛠",fitzpatrick_scale:false,category:"objects"},pick:{keywords:["tools","dig"],char:"⛏",fitzpatrick_scale:false,category:"objects"},nut_and_bolt:{keywords:["handy","tools","fix"],char:"🔩",fitzpatrick_scale:false,category:"objects"},gear:{keywords:["cog"],char:"⚙",fitzpatrick_scale:false,category:"objects"},brick:{keywords:["bricks"],char:"🧱",fitzpatrick_scale:false,category:"objects"},chains:{keywords:["lock","arrest"],char:"⛓",fitzpatrick_scale:false,category:"objects"},magnet:{keywords:["attraction","magnetic"],char:"🧲",fitzpatrick_scale:false,category:"objects"},gun:{keywords:["violence","weapon","pistol","revolver"],char:"🔫",fitzpatrick_scale:false,category:"objects"},bomb:{keywords:["boom","explode","explosion","terrorism"],char:"💣",fitzpatrick_scale:false,category:"objects"},firecracker:{keywords:["dynamite","boom","explode","explosion","explosive"],char:"🧨",fitzpatrick_scale:false,category:"objects"},hocho:{keywords:["knife","blade","cutlery","kitchen","weapon"],char:"🔪",fitzpatrick_scale:false,category:"objects"},dagger:{keywords:["weapon"],char:"🗡",fitzpatrick_scale:false,category:"objects"},crossed_swords:{keywords:["weapon"],char:"⚔",fitzpatrick_scale:false,category:"objects"},shield:{keywords:["protection","security"],char:"🛡",fitzpatrick_scale:false,category:"objects"},smoking:{keywords:["kills","tobacco","cigarette","joint","smoke"],char:"🚬",fitzpatrick_scale:false,category:"objects"},skull_and_crossbones:{keywords:["poison","danger","deadly","scary","death","pirate","evil"],char:"☠",fitzpatrick_scale:false,category:"objects"},coffin:{keywords:["vampire","dead","die","death","rip","graveyard","cemetery","casket","funeral","box"],char:"⚰",fitzpatrick_scale:false,category:"objects"},funeral_urn:{keywords:["dead","die","death","rip","ashes"],char:"⚱",fitzpatrick_scale:false,category:"objects"},amphora:{keywords:["vase","jar"],char:"🏺",fitzpatrick_scale:false,category:"objects"},crystal_ball:{keywords:["disco","party","magic","circus","fortune_teller"],char:"🔮",fitzpatrick_scale:false,category:"objects"},prayer_beads:{keywords:["dhikr","religious"],char:"📿",fitzpatrick_scale:false,category:"objects"},nazar_amulet:{keywords:["bead","charm"],char:"🧿",fitzpatrick_scale:false,category:"objects"},barber:{keywords:["hair","salon","style"],char:"💈",fitzpatrick_scale:false,category:"objects"},alembic:{keywords:["distilling","science","experiment","chemistry"],char:"⚗",fitzpatrick_scale:false,category:"objects"},telescope:{keywords:["stars","space","zoom","science","astronomy"],char:"🔭",fitzpatrick_scale:false,category:"objects"},microscope:{keywords:["laboratory","experiment","zoomin","science","study"],char:"🔬",fitzpatrick_scale:false,category:"objects"},hole:{keywords:["embarrassing"],char:"🕳",fitzpatrick_scale:false,category:"objects"},pill:{keywords:["health","medicine","doctor","pharmacy","drug"],char:"💊",fitzpatrick_scale:false,category:"objects"},syringe:{keywords:["health","hospital","drugs","blood","medicine","needle","doctor","nurse"],char:"💉",fitzpatrick_scale:false,category:"objects"},dna:{keywords:["biologist","genetics","life"],char:"🧬",fitzpatrick_scale:false,category:"objects"},microbe:{keywords:["amoeba","bacteria","germs"],char:"🦠",fitzpatrick_scale:false,category:"objects"},petri_dish:{keywords:["bacteria","biology","culture","lab"],char:"🧫",fitzpatrick_scale:false,category:"objects"},test_tube:{keywords:["chemistry","experiment","lab","science"],char:"🧪",fitzpatrick_scale:false,category:"objects"},thermometer:{keywords:["weather","temperature","hot","cold"],char:"🌡",fitzpatrick_scale:false,category:"objects"},broom:{keywords:["cleaning","sweeping","witch"],char:"🧹",fitzpatrick_scale:false,category:"objects"},basket:{keywords:["laundry"],char:"🧺",fitzpatrick_scale:false,category:"objects"},toilet_paper:{keywords:["roll"],char:"🧻",fitzpatrick_scale:false,category:"objects"},label:{keywords:["sale","tag"],char:"🏷",fitzpatrick_scale:false,category:"objects"},bookmark:{keywords:["favorite","label","save"],char:"🔖",fitzpatrick_scale:false,category:"objects"},toilet:{keywords:["restroom","wc","washroom","bathroom","potty"],char:"🚽",fitzpatrick_scale:false,category:"objects"},shower:{keywords:["clean","water","bathroom"],char:"🚿",fitzpatrick_scale:false,category:"objects"},bathtub:{keywords:["clean","shower","bathroom"],char:"🛁",fitzpatrick_scale:false,category:"objects"},soap:{keywords:["bar","bathing","cleaning","lather"],char:"🧼",fitzpatrick_scale:false,category:"objects"},sponge:{keywords:["absorbing","cleaning","porous"],char:"🧽",fitzpatrick_scale:false,category:"objects"},lotion_bottle:{keywords:["moisturizer","sunscreen"],char:"🧴",fitzpatrick_scale:false,category:"objects"},key:{keywords:["lock","door","password"],char:"🔑",fitzpatrick_scale:false,category:"objects"},old_key:{keywords:["lock","door","password"],char:"🗝",fitzpatrick_scale:false,category:"objects"},couch_and_lamp:{keywords:["read","chill"],char:"🛋",fitzpatrick_scale:false,category:"objects"},sleeping_bed:{keywords:["bed","rest"],char:"🛌",fitzpatrick_scale:true,category:"objects"},bed:{keywords:["sleep","rest"],char:"🛏",fitzpatrick_scale:false,category:"objects"},door:{keywords:["house","entry","exit"],char:"🚪",fitzpatrick_scale:false,category:"objects"},bellhop_bell:{keywords:["service"],char:"🛎",fitzpatrick_scale:false,category:"objects"},teddy_bear:{keywords:["plush","stuffed"],char:"🧸",fitzpatrick_scale:false,category:"objects"},framed_picture:{keywords:["photography"],char:"🖼",fitzpatrick_scale:false,category:"objects"},world_map:{keywords:["location","direction"],char:"🗺",fitzpatrick_scale:false,category:"objects"},parasol_on_ground:{keywords:["weather","summer"],char:"⛱",fitzpatrick_scale:false,category:"objects"},moyai:{keywords:["rock","easter island","moai"],char:"🗿",fitzpatrick_scale:false,category:"objects"},shopping:{keywords:["mall","buy","purchase"],char:"🛍",fitzpatrick_scale:false,category:"objects"},shopping_cart:{keywords:["trolley"],char:"🛒",fitzpatrick_scale:false,category:"objects"},balloon:{keywords:["party","celebration","birthday","circus"],char:"🎈",fitzpatrick_scale:false,category:"objects"},flags:{keywords:["fish","japanese","koinobori","carp","banner"],char:"🎏",fitzpatrick_scale:false,category:"objects"},ribbon:{keywords:["decoration","pink","girl","bowtie"],char:"🎀",fitzpatrick_scale:false,category:"objects"},gift:{keywords:["present","birthday","christmas","xmas"],char:"🎁",fitzpatrick_scale:false,category:"objects"},confetti_ball:{keywords:["festival","party","birthday","circus"],char:"🎊",fitzpatrick_scale:false,category:"objects"},tada:{keywords:["party","congratulations","birthday","magic","circus","celebration"],char:"🎉",fitzpatrick_scale:false,category:"objects"},dolls:{keywords:["japanese","toy","kimono"],char:"🎎",fitzpatrick_scale:false,category:"objects"},wind_chime:{keywords:["nature","ding","spring","bell"],char:"🎐",fitzpatrick_scale:false,category:"objects"},crossed_flags:{keywords:["japanese","nation","country","border"],char:"🎌",fitzpatrick_scale:false,category:"objects"},izakaya_lantern:{keywords:["light","paper","halloween","spooky"],char:"🏮",fitzpatrick_scale:false,category:"objects"},red_envelope:{keywords:["gift"],char:"🧧",fitzpatrick_scale:false,category:"objects"},email:{keywords:["letter","postal","inbox","communication"],char:"✉️",fitzpatrick_scale:false,category:"objects"},envelope_with_arrow:{keywords:["email","communication"],char:"📩",fitzpatrick_scale:false,category:"objects"},incoming_envelope:{keywords:["email","inbox"],char:"📨",fitzpatrick_scale:false,category:"objects"},"e-mail":{keywords:["communication","inbox"],char:"📧",fitzpatrick_scale:false,category:"objects"},love_letter:{keywords:["email","like","affection","envelope","valentines"],char:"💌",fitzpatrick_scale:false,category:"objects"},postbox:{keywords:["email","letter","envelope"],char:"📮",fitzpatrick_scale:false,category:"objects"},mailbox_closed:{keywords:["email","communication","inbox"],char:"📪",fitzpatrick_scale:false,category:"objects"},mailbox:{keywords:["email","inbox","communication"],char:"📫",fitzpatrick_scale:false,category:"objects"},mailbox_with_mail:{keywords:["email","inbox","communication"],char:"📬",fitzpatrick_scale:false,category:"objects"},mailbox_with_no_mail:{keywords:["email","inbox"],char:"📭",fitzpatrick_scale:false,category:"objects"},package:{keywords:["mail","gift","cardboard","box","moving"],char:"📦",fitzpatrick_scale:false,category:"objects"},postal_horn:{keywords:["instrument","music"],char:"📯",fitzpatrick_scale:false,category:"objects"},inbox_tray:{keywords:["email","documents"],char:"📥",fitzpatrick_scale:false,category:"objects"},outbox_tray:{keywords:["inbox","email"],char:"📤",fitzpatrick_scale:false,category:"objects"},scroll:{keywords:["documents","ancient","history","paper"],char:"📜",fitzpatrick_scale:false,category:"objects"},page_with_curl:{keywords:["documents","office","paper"],char:"📃",fitzpatrick_scale:false,category:"objects"},bookmark_tabs:{keywords:["favorite","save","order","tidy"],char:"📑",fitzpatrick_scale:false,category:"objects"},receipt:{keywords:["accounting","expenses"],char:"🧾",fitzpatrick_scale:false,category:"objects"},bar_chart:{keywords:["graph","presentation","stats"],char:"📊",fitzpatrick_scale:false,category:"objects"},chart_with_upwards_trend:{keywords:["graph","presentation","stats","recovery","business","economics","money","sales","good","success"],char:"📈",fitzpatrick_scale:false,category:"objects"},chart_with_downwards_trend:{keywords:["graph","presentation","stats","recession","business","economics","money","sales","bad","failure"],char:"📉",fitzpatrick_scale:false,category:"objects"},page_facing_up:{keywords:["documents","office","paper","information"],char:"📄",fitzpatrick_scale:false,category:"objects"},date:{keywords:["calendar","schedule"],char:"📅",fitzpatrick_scale:false,category:"objects"},calendar:{keywords:["schedule","date","planning"],char:"📆",fitzpatrick_scale:false,category:"objects"},spiral_calendar:{keywords:["date","schedule","planning"],char:"🗓",fitzpatrick_scale:false,category:"objects"},card_index:{keywords:["business","stationery"],char:"📇",fitzpatrick_scale:false,category:"objects"},card_file_box:{keywords:["business","stationery"],char:"🗃",fitzpatrick_scale:false,category:"objects"},ballot_box:{keywords:["election","vote"],char:"🗳",fitzpatrick_scale:false,category:"objects"},file_cabinet:{keywords:["filing","organizing"],char:"🗄",fitzpatrick_scale:false,category:"objects"},clipboard:{keywords:["stationery","documents"],char:"📋",fitzpatrick_scale:false,category:"objects"},spiral_notepad:{keywords:["memo","stationery"],char:"🗒",fitzpatrick_scale:false,category:"objects"},file_folder:{keywords:["documents","business","office"],char:"📁",fitzpatrick_scale:false,category:"objects"},open_file_folder:{keywords:["documents","load"],char:"📂",fitzpatrick_scale:false,category:"objects"},card_index_dividers:{keywords:["organizing","business","stationery"],char:"🗂",fitzpatrick_scale:false,category:"objects"},newspaper_roll:{keywords:["press","headline"],char:"🗞",fitzpatrick_scale:false,category:"objects"},newspaper:{keywords:["press","headline"],char:"📰",fitzpatrick_scale:false,category:"objects"},notebook:{keywords:["stationery","record","notes","paper","study"],char:"📓",fitzpatrick_scale:false,category:"objects"},closed_book:{keywords:["read","library","knowledge","textbook","learn"],char:"📕",fitzpatrick_scale:false,category:"objects"},green_book:{keywords:["read","library","knowledge","study"],char:"📗",fitzpatrick_scale:false,category:"objects"},blue_book:{keywords:["read","library","knowledge","learn","study"],char:"📘",fitzpatrick_scale:false,category:"objects"},orange_book:{keywords:["read","library","knowledge","textbook","study"],char:"📙",fitzpatrick_scale:false,category:"objects"},notebook_with_decorative_cover:{keywords:["classroom","notes","record","paper","study"],char:"📔",fitzpatrick_scale:false,category:"objects"},ledger:{keywords:["notes","paper"],char:"📒",fitzpatrick_scale:false,category:"objects"},books:{keywords:["literature","library","study"],char:"📚",fitzpatrick_scale:false,category:"objects"},open_book:{keywords:["book","read","library","knowledge","literature","learn","study"],char:"📖",fitzpatrick_scale:false,category:"objects"},safety_pin:{keywords:["diaper"],char:"🧷",fitzpatrick_scale:false,category:"objects"},link:{keywords:["rings","url"],char:"🔗",fitzpatrick_scale:false,category:"objects"},paperclip:{keywords:["documents","stationery"],char:"📎",fitzpatrick_scale:false,category:"objects"},paperclips:{keywords:["documents","stationery"],char:"🖇",fitzpatrick_scale:false,category:"objects"},scissors:{keywords:["stationery","cut"],char:"✂️",fitzpatrick_scale:false,category:"objects"},triangular_ruler:{keywords:["stationery","math","architect","sketch"],char:"📐",fitzpatrick_scale:false,category:"objects"},straight_ruler:{keywords:["stationery","calculate","length","math","school","drawing","architect","sketch"],char:"📏",fitzpatrick_scale:false,category:"objects"},abacus:{keywords:["calculation"],char:"🧮",fitzpatrick_scale:false,category:"objects"},pushpin:{keywords:["stationery","mark","here"],char:"📌",fitzpatrick_scale:false,category:"objects"},round_pushpin:{keywords:["stationery","location","map","here"],char:"📍",fitzpatrick_scale:false,category:"objects"},triangular_flag_on_post:{keywords:["mark","milestone","place"],char:"🚩",fitzpatrick_scale:false,category:"objects"},white_flag:{keywords:["losing","loser","lost","surrender","give up","fail"],char:"🏳",fitzpatrick_scale:false,category:"objects"},black_flag:{keywords:["pirate"],char:"🏴",fitzpatrick_scale:false,category:"objects"},rainbow_flag:{keywords:["flag","rainbow","pride","gay","lgbt","glbt","queer","homosexual","lesbian","bisexual","transgender"],char:"🏳️‍🌈",fitzpatrick_scale:false,category:"objects"},closed_lock_with_key:{keywords:["security","privacy"],char:"🔐",fitzpatrick_scale:false,category:"objects"},lock:{keywords:["security","password","padlock"],char:"🔒",fitzpatrick_scale:false,category:"objects"},unlock:{keywords:["privacy","security"],char:"🔓",fitzpatrick_scale:false,category:"objects"},lock_with_ink_pen:{keywords:["security","secret"],char:"🔏",fitzpatrick_scale:false,category:"objects"},pen:{keywords:["stationery","writing","write"],char:"🖊",fitzpatrick_scale:false,category:"objects"},fountain_pen:{keywords:["stationery","writing","write"],char:"🖋",fitzpatrick_scale:false,category:"objects"},black_nib:{keywords:["pen","stationery","writing","write"],char:"✒️",fitzpatrick_scale:false,category:"objects"},memo:{keywords:["write","documents","stationery","pencil","paper","writing","legal","exam","quiz","test","study","compose"],char:"📝",fitzpatrick_scale:false,category:"objects"},pencil2:{keywords:["stationery","write","paper","writing","school","study"],char:"✏️",fitzpatrick_scale:false,category:"objects"},crayon:{keywords:["drawing","creativity"],char:"🖍",fitzpatrick_scale:false,category:"objects"},paintbrush:{keywords:["drawing","creativity","art"],char:"🖌",fitzpatrick_scale:false,category:"objects"},mag:{keywords:["search","zoom","find","detective"],char:"🔍",fitzpatrick_scale:false,category:"objects"},mag_right:{keywords:["search","zoom","find","detective"],char:"🔎",fitzpatrick_scale:false,category:"objects"},heart:{keywords:["love","like","valentines"],char:"❤️",fitzpatrick_scale:false,category:"symbols"},orange_heart:{keywords:["love","like","affection","valentines"],char:"🧡",fitzpatrick_scale:false,category:"symbols"},yellow_heart:{keywords:["love","like","affection","valentines"],char:"💛",fitzpatrick_scale:false,category:"symbols"},green_heart:{keywords:["love","like","affection","valentines"],char:"💚",fitzpatrick_scale:false,category:"symbols"},blue_heart:{keywords:["love","like","affection","valentines"],char:"💙",fitzpatrick_scale:false,category:"symbols"},purple_heart:{keywords:["love","like","affection","valentines"],char:"💜",fitzpatrick_scale:false,category:"symbols"},black_heart:{keywords:["evil"],char:"🖤",fitzpatrick_scale:false,category:"symbols"},broken_heart:{keywords:["sad","sorry","break","heart","heartbreak"],char:"💔",fitzpatrick_scale:false,category:"symbols"},heavy_heart_exclamation:{keywords:["decoration","love"],char:"❣",fitzpatrick_scale:false,category:"symbols"},two_hearts:{keywords:["love","like","affection","valentines","heart"],char:"💕",fitzpatrick_scale:false,category:"symbols"},revolving_hearts:{keywords:["love","like","affection","valentines"],char:"💞",fitzpatrick_scale:false,category:"symbols"},heartbeat:{keywords:["love","like","affection","valentines","pink","heart"],char:"💓",fitzpatrick_scale:false,category:"symbols"},heartpulse:{keywords:["like","love","affection","valentines","pink"],char:"💗",fitzpatrick_scale:false,category:"symbols"},sparkling_heart:{keywords:["love","like","affection","valentines"],char:"💖",fitzpatrick_scale:false,category:"symbols"},cupid:{keywords:["love","like","heart","affection","valentines"],char:"💘",fitzpatrick_scale:false,category:"symbols"},gift_heart:{keywords:["love","valentines"],char:"💝",fitzpatrick_scale:false,category:"symbols"},heart_decoration:{keywords:["purple-square","love","like"],char:"💟",fitzpatrick_scale:false,category:"symbols"},peace_symbol:{keywords:["hippie"],char:"☮",fitzpatrick_scale:false,category:"symbols"},latin_cross:{keywords:["christianity"],char:"✝",fitzpatrick_scale:false,category:"symbols"},star_and_crescent:{keywords:["islam"],char:"☪",fitzpatrick_scale:false,category:"symbols"},om:{keywords:["hinduism","buddhism","sikhism","jainism"],char:"🕉",fitzpatrick_scale:false,category:"symbols"},wheel_of_dharma:{keywords:["hinduism","buddhism","sikhism","jainism"],char:"☸",fitzpatrick_scale:false,category:"symbols"},star_of_david:{keywords:["judaism"],char:"✡",fitzpatrick_scale:false,category:"symbols"},six_pointed_star:{keywords:["purple-square","religion","jewish","hexagram"],char:"🔯",fitzpatrick_scale:false,category:"symbols"},menorah:{keywords:["hanukkah","candles","jewish"],char:"🕎",fitzpatrick_scale:false,category:"symbols"},yin_yang:{keywords:["balance"],char:"☯",fitzpatrick_scale:false,category:"symbols"},orthodox_cross:{keywords:["suppedaneum","religion"],char:"☦",fitzpatrick_scale:false,category:"symbols"},place_of_worship:{keywords:["religion","church","temple","prayer"],char:"🛐",fitzpatrick_scale:false,category:"symbols"},ophiuchus:{keywords:["sign","purple-square","constellation","astrology"],char:"⛎",fitzpatrick_scale:false,category:"symbols"},aries:{keywords:["sign","purple-square","zodiac","astrology"],char:"♈",fitzpatrick_scale:false,category:"symbols"},taurus:{keywords:["purple-square","sign","zodiac","astrology"],char:"♉",fitzpatrick_scale:false,category:"symbols"},gemini:{keywords:["sign","zodiac","purple-square","astrology"],char:"♊",fitzpatrick_scale:false,category:"symbols"},cancer:{keywords:["sign","zodiac","purple-square","astrology"],char:"♋",fitzpatrick_scale:false,category:"symbols"},leo:{keywords:["sign","purple-square","zodiac","astrology"],char:"♌",fitzpatrick_scale:false,category:"symbols"},virgo:{keywords:["sign","zodiac","purple-square","astrology"],char:"♍",fitzpatrick_scale:false,category:"symbols"},libra:{keywords:["sign","purple-square","zodiac","astrology"],char:"♎",fitzpatrick_scale:false,category:"symbols"},scorpius:{keywords:["sign","zodiac","purple-square","astrology","scorpio"],char:"♏",fitzpatrick_scale:false,category:"symbols"},sagittarius:{keywords:["sign","zodiac","purple-square","astrology"],char:"♐",fitzpatrick_scale:false,category:"symbols"},capricorn:{keywords:["sign","zodiac","purple-square","astrology"],char:"♑",fitzpatrick_scale:false,category:"symbols"},aquarius:{keywords:["sign","purple-square","zodiac","astrology"],char:"♒",fitzpatrick_scale:false,category:"symbols"},pisces:{keywords:["purple-square","sign","zodiac","astrology"],char:"♓",fitzpatrick_scale:false,category:"symbols"},id:{keywords:["purple-square","words"],char:"🆔",fitzpatrick_scale:false,category:"symbols"},atom_symbol:{keywords:["science","physics","chemistry"],char:"⚛",fitzpatrick_scale:false,category:"symbols"},u7a7a:{keywords:["kanji","japanese","chinese","empty","sky","blue-square"],char:"🈳",fitzpatrick_scale:false,category:"symbols"},u5272:{keywords:["cut","divide","chinese","kanji","pink-square"],char:"🈹",fitzpatrick_scale:false,category:"symbols"},radioactive:{keywords:["nuclear","danger"],char:"☢",fitzpatrick_scale:false,category:"symbols"},biohazard:{keywords:["danger"],char:"☣",fitzpatrick_scale:false,category:"symbols"},mobile_phone_off:{keywords:["mute","orange-square","silence","quiet"],char:"📴",fitzpatrick_scale:false,category:"symbols"},vibration_mode:{keywords:["orange-square","phone"],char:"📳",fitzpatrick_scale:false,category:"symbols"},u6709:{keywords:["orange-square","chinese","have","kanji"],char:"🈶",fitzpatrick_scale:false,category:"symbols"},u7121:{keywords:["nothing","chinese","kanji","japanese","orange-square"],char:"🈚",fitzpatrick_scale:false,category:"symbols"},u7533:{keywords:["chinese","japanese","kanji","orange-square"],char:"🈸",fitzpatrick_scale:false,category:"symbols"},u55b6:{keywords:["japanese","opening hours","orange-square"],char:"🈺",fitzpatrick_scale:false,category:"symbols"},u6708:{keywords:["chinese","month","moon","japanese","orange-square","kanji"],char:"🈷️",fitzpatrick_scale:false,category:"symbols"},eight_pointed_black_star:{keywords:["orange-square","shape","polygon"],char:"✴️",fitzpatrick_scale:false,category:"symbols"},vs:{keywords:["words","orange-square"],char:"🆚",fitzpatrick_scale:false,category:"symbols"},accept:{keywords:["ok","good","chinese","kanji","agree","yes","orange-circle"],char:"🉑",fitzpatrick_scale:false,category:"symbols"},white_flower:{keywords:["japanese","spring"],char:"💮",fitzpatrick_scale:false,category:"symbols"},ideograph_advantage:{keywords:["chinese","kanji","obtain","get","circle"],char:"🉐",fitzpatrick_scale:false,category:"symbols"},secret:{keywords:["privacy","chinese","sshh","kanji","red-circle"],char:"㊙️",fitzpatrick_scale:false,category:"symbols"},congratulations:{keywords:["chinese","kanji","japanese","red-circle"],char:"㊗️",fitzpatrick_scale:false,category:"symbols"},u5408:{keywords:["japanese","chinese","join","kanji","red-square"],char:"🈴",fitzpatrick_scale:false,category:"symbols"},u6e80:{keywords:["full","chinese","japanese","red-square","kanji"],char:"🈵",fitzpatrick_scale:false,category:"symbols"},u7981:{keywords:["kanji","japanese","chinese","forbidden","limit","restricted","red-square"],char:"🈲",fitzpatrick_scale:false,category:"symbols"},a:{keywords:["red-square","alphabet","letter"],char:"🅰️",fitzpatrick_scale:false,category:"symbols"},b:{keywords:["red-square","alphabet","letter"],char:"🅱️",fitzpatrick_scale:false,category:"symbols"},ab:{keywords:["red-square","alphabet"],char:"🆎",fitzpatrick_scale:false,category:"symbols"},cl:{keywords:["alphabet","words","red-square"],char:"🆑",fitzpatrick_scale:false,category:"symbols"},o2:{keywords:["alphabet","red-square","letter"],char:"🅾️",fitzpatrick_scale:false,category:"symbols"},sos:{keywords:["help","red-square","words","emergency","911"],char:"🆘",fitzpatrick_scale:false,category:"symbols"},no_entry:{keywords:["limit","security","privacy","bad","denied","stop","circle"],char:"⛔",fitzpatrick_scale:false,category:"symbols"},name_badge:{keywords:["fire","forbid"],char:"📛",fitzpatrick_scale:false,category:"symbols"},no_entry_sign:{keywords:["forbid","stop","limit","denied","disallow","circle"],char:"🚫",fitzpatrick_scale:false,category:"symbols"},x:{keywords:["no","delete","remove","cancel","red"],char:"❌",fitzpatrick_scale:false,category:"symbols"},o:{keywords:["circle","round"],char:"⭕",fitzpatrick_scale:false,category:"symbols"},stop_sign:{keywords:["stop"],char:"🛑",fitzpatrick_scale:false,category:"symbols"},anger:{keywords:["angry","mad"],char:"💢",fitzpatrick_scale:false,category:"symbols"},hotsprings:{keywords:["bath","warm","relax"],char:"♨️",fitzpatrick_scale:false,category:"symbols"},no_pedestrians:{keywords:["rules","crossing","walking","circle"],char:"🚷",fitzpatrick_scale:false,category:"symbols"},do_not_litter:{keywords:["trash","bin","garbage","circle"],char:"🚯",fitzpatrick_scale:false,category:"symbols"},no_bicycles:{keywords:["cyclist","prohibited","circle"],char:"🚳",fitzpatrick_scale:false,category:"symbols"},"non-potable_water":{keywords:["drink","faucet","tap","circle"],char:"🚱",fitzpatrick_scale:false,category:"symbols"},underage:{keywords:["18","drink","pub","night","minor","circle"],char:"🔞",fitzpatrick_scale:false,category:"symbols"},no_mobile_phones:{keywords:["iphone","mute","circle"],char:"📵",fitzpatrick_scale:false,category:"symbols"},exclamation:{keywords:["heavy_exclamation_mark","danger","surprise","punctuation","wow","warning"],char:"❗",fitzpatrick_scale:false,category:"symbols"},grey_exclamation:{keywords:["surprise","punctuation","gray","wow","warning"],char:"❕",fitzpatrick_scale:false,category:"symbols"},question:{keywords:["doubt","confused"],char:"❓",fitzpatrick_scale:false,category:"symbols"},grey_question:{keywords:["doubts","gray","huh","confused"],char:"❔",fitzpatrick_scale:false,category:"symbols"},bangbang:{keywords:["exclamation","surprise"],char:"‼️",fitzpatrick_scale:false,category:"symbols"},interrobang:{keywords:["wat","punctuation","surprise"],char:"⁉️",fitzpatrick_scale:false,category:"symbols"},100:{keywords:["score","perfect","numbers","century","exam","quiz","test","pass","hundred"],char:"💯",fitzpatrick_scale:false,category:"symbols"},low_brightness:{keywords:["sun","afternoon","warm","summer"],char:"🔅",fitzpatrick_scale:false,category:"symbols"},high_brightness:{keywords:["sun","light"],char:"🔆",fitzpatrick_scale:false,category:"symbols"},trident:{keywords:["weapon","spear"],char:"🔱",fitzpatrick_scale:false,category:"symbols"},fleur_de_lis:{keywords:["decorative","scout"],char:"⚜",fitzpatrick_scale:false,category:"symbols"},part_alternation_mark:{keywords:["graph","presentation","stats","business","economics","bad"],char:"〽️",fitzpatrick_scale:false,category:"symbols"},warning:{keywords:["exclamation","wip","alert","error","problem","issue"],char:"⚠️",fitzpatrick_scale:false,category:"symbols"},children_crossing:{keywords:["school","warning","danger","sign","driving","yellow-diamond"],char:"🚸",fitzpatrick_scale:false,category:"symbols"},beginner:{keywords:["badge","shield"],char:"🔰",fitzpatrick_scale:false,category:"symbols"},recycle:{keywords:["arrow","environment","garbage","trash"],char:"♻️",fitzpatrick_scale:false,category:"symbols"},u6307:{keywords:["chinese","point","green-square","kanji"],char:"🈯",fitzpatrick_scale:false,category:"symbols"},chart:{keywords:["green-square","graph","presentation","stats"],char:"💹",fitzpatrick_scale:false,category:"symbols"},sparkle:{keywords:["stars","green-square","awesome","good","fireworks"],char:"❇️",fitzpatrick_scale:false,category:"symbols"},eight_spoked_asterisk:{keywords:["star","sparkle","green-square"],char:"✳️",fitzpatrick_scale:false,category:"symbols"},negative_squared_cross_mark:{keywords:["x","green-square","no","deny"],char:"❎",fitzpatrick_scale:false,category:"symbols"},white_check_mark:{keywords:["green-square","ok","agree","vote","election","answer","tick"],char:"✅",fitzpatrick_scale:false,category:"symbols"},diamond_shape_with_a_dot_inside:{keywords:["jewel","blue","gem","crystal","fancy"],char:"💠",fitzpatrick_scale:false,category:"symbols"},cyclone:{keywords:["weather","swirl","blue","cloud","vortex","spiral","whirlpool","spin","tornado","hurricane","typhoon"],char:"🌀",fitzpatrick_scale:false,category:"symbols"},loop:{keywords:["tape","cassette"],char:"➿",fitzpatrick_scale:false,category:"symbols"},globe_with_meridians:{keywords:["earth","international","world","internet","interweb","i18n"],char:"🌐",fitzpatrick_scale:false,category:"symbols"},m:{keywords:["alphabet","blue-circle","letter"],char:"Ⓜ️",fitzpatrick_scale:false,category:"symbols"},atm:{keywords:["money","sales","cash","blue-square","payment","bank"],char:"🏧",fitzpatrick_scale:false,category:"symbols"},sa:{keywords:["japanese","blue-square","katakana"],char:"🈂️",fitzpatrick_scale:false,category:"symbols"},passport_control:{keywords:["custom","blue-square"],char:"🛂",fitzpatrick_scale:false,category:"symbols"},customs:{keywords:["passport","border","blue-square"],char:"🛃",fitzpatrick_scale:false,category:"symbols"},baggage_claim:{keywords:["blue-square","airport","transport"],char:"🛄",fitzpatrick_scale:false,category:"symbols"},left_luggage:{keywords:["blue-square","travel"],char:"🛅",fitzpatrick_scale:false,category:"symbols"},wheelchair:{keywords:["blue-square","disabled","a11y","accessibility"],char:"♿",fitzpatrick_scale:false,category:"symbols"},no_smoking:{keywords:["cigarette","blue-square","smell","smoke"],char:"🚭",fitzpatrick_scale:false,category:"symbols"},wc:{keywords:["toilet","restroom","blue-square"],char:"🚾",fitzpatrick_scale:false,category:"symbols"},parking:{keywords:["cars","blue-square","alphabet","letter"],char:"🅿️",fitzpatrick_scale:false,category:"symbols"},potable_water:{keywords:["blue-square","liquid","restroom","cleaning","faucet"],char:"🚰",fitzpatrick_scale:false,category:"symbols"},mens:{keywords:["toilet","restroom","wc","blue-square","gender","male"],char:"🚹",fitzpatrick_scale:false,category:"symbols"},womens:{keywords:["purple-square","woman","female","toilet","loo","restroom","gender"],char:"🚺",fitzpatrick_scale:false,category:"symbols"},baby_symbol:{keywords:["orange-square","child"],char:"🚼",fitzpatrick_scale:false,category:"symbols"},restroom:{keywords:["blue-square","toilet","refresh","wc","gender"],char:"🚻",fitzpatrick_scale:false,category:"symbols"},put_litter_in_its_place:{keywords:["blue-square","sign","human","info"],char:"🚮",fitzpatrick_scale:false,category:"symbols"},cinema:{keywords:["blue-square","record","film","movie","curtain","stage","theater"],char:"🎦",fitzpatrick_scale:false,category:"symbols"},signal_strength:{keywords:["blue-square","reception","phone","internet","connection","wifi","bluetooth","bars"],char:"📶",fitzpatrick_scale:false,category:"symbols"},koko:{keywords:["blue-square","here","katakana","japanese","destination"],char:"🈁",fitzpatrick_scale:false,category:"symbols"},ng:{keywords:["blue-square","words","shape","icon"],char:"🆖",fitzpatrick_scale:false,category:"symbols"},ok:{keywords:["good","agree","yes","blue-square"],char:"🆗",fitzpatrick_scale:false,category:"symbols"},up:{keywords:["blue-square","above","high"],char:"🆙",fitzpatrick_scale:false,category:"symbols"},cool:{keywords:["words","blue-square"],char:"🆒",fitzpatrick_scale:false,category:"symbols"},new:{keywords:["blue-square","words","start"],char:"🆕",fitzpatrick_scale:false,category:"symbols"},free:{keywords:["blue-square","words"],char:"🆓",fitzpatrick_scale:false,category:"symbols"},zero:{keywords:["0","numbers","blue-square","null"],char:"0️⃣",fitzpatrick_scale:false,category:"symbols"},one:{keywords:["blue-square","numbers","1"],char:"1️⃣",fitzpatrick_scale:false,category:"symbols"},two:{keywords:["numbers","2","prime","blue-square"],char:"2️⃣",fitzpatrick_scale:false,category:"symbols"},three:{keywords:["3","numbers","prime","blue-square"],char:"3️⃣",fitzpatrick_scale:false,category:"symbols"},four:{keywords:["4","numbers","blue-square"],char:"4️⃣",fitzpatrick_scale:false,category:"symbols"},five:{keywords:["5","numbers","blue-square","prime"],char:"5️⃣",fitzpatrick_scale:false,category:"symbols"},six:{keywords:["6","numbers","blue-square"],char:"6️⃣",fitzpatrick_scale:false,category:"symbols"},seven:{keywords:["7","numbers","blue-square","prime"],char:"7️⃣",fitzpatrick_scale:false,category:"symbols"},eight:{keywords:["8","blue-square","numbers"],char:"8️⃣",fitzpatrick_scale:false,category:"symbols"},nine:{keywords:["blue-square","numbers","9"],char:"9️⃣",fitzpatrick_scale:false,category:"symbols"},keycap_ten:{keywords:["numbers","10","blue-square"],char:"🔟",fitzpatrick_scale:false,category:"symbols"},asterisk:{keywords:["star","keycap"],char:"*⃣",fitzpatrick_scale:false,category:"symbols"},1234:{keywords:["numbers","blue-square"],char:"🔢",fitzpatrick_scale:false,category:"symbols"},eject_button:{keywords:["blue-square"],char:"⏏️",fitzpatrick_scale:false,category:"symbols"},arrow_forward:{keywords:["blue-square","right","direction","play"],char:"▶️",fitzpatrick_scale:false,category:"symbols"},pause_button:{keywords:["pause","blue-square"],char:"⏸",fitzpatrick_scale:false,category:"symbols"},next_track_button:{keywords:["forward","next","blue-square"],char:"⏭",fitzpatrick_scale:false,category:"symbols"},stop_button:{keywords:["blue-square"],char:"⏹",fitzpatrick_scale:false,category:"symbols"},record_button:{keywords:["blue-square"],char:"⏺",fitzpatrick_scale:false,category:"symbols"},play_or_pause_button:{keywords:["blue-square","play","pause"],char:"⏯",fitzpatrick_scale:false,category:"symbols"},previous_track_button:{keywords:["backward"],char:"⏮",fitzpatrick_scale:false,category:"symbols"},fast_forward:{keywords:["blue-square","play","speed","continue"],char:"⏩",fitzpatrick_scale:false,category:"symbols"},rewind:{keywords:["play","blue-square"],char:"⏪",fitzpatrick_scale:false,category:"symbols"},twisted_rightwards_arrows:{keywords:["blue-square","shuffle","music","random"],char:"🔀",fitzpatrick_scale:false,category:"symbols"},repeat:{keywords:["loop","record"],char:"🔁",fitzpatrick_scale:false,category:"symbols"},repeat_one:{keywords:["blue-square","loop"],char:"🔂",fitzpatrick_scale:false,category:"symbols"},arrow_backward:{keywords:["blue-square","left","direction"],char:"◀️",fitzpatrick_scale:false,category:"symbols"},arrow_up_small:{keywords:["blue-square","triangle","direction","point","forward","top"],char:"🔼",fitzpatrick_scale:false,category:"symbols"},arrow_down_small:{keywords:["blue-square","direction","bottom"],char:"🔽",fitzpatrick_scale:false,category:"symbols"},arrow_double_up:{keywords:["blue-square","direction","top"],char:"⏫",fitzpatrick_scale:false,category:"symbols"},arrow_double_down:{keywords:["blue-square","direction","bottom"],char:"⏬",fitzpatrick_scale:false,category:"symbols"},arrow_right:{keywords:["blue-square","next"],char:"➡️",fitzpatrick_scale:false,category:"symbols"},arrow_left:{keywords:["blue-square","previous","back"],char:"⬅️",fitzpatrick_scale:false,category:"symbols"},arrow_up:{keywords:["blue-square","continue","top","direction"],char:"⬆️",fitzpatrick_scale:false,category:"symbols"},arrow_down:{keywords:["blue-square","direction","bottom"],char:"⬇️",fitzpatrick_scale:false,category:"symbols"},arrow_upper_right:{keywords:["blue-square","point","direction","diagonal","northeast"],char:"↗️",fitzpatrick_scale:false,category:"symbols"},arrow_lower_right:{keywords:["blue-square","direction","diagonal","southeast"],char:"↘️",fitzpatrick_scale:false,category:"symbols"},arrow_lower_left:{keywords:["blue-square","direction","diagonal","southwest"],char:"↙️",fitzpatrick_scale:false,category:"symbols"},arrow_upper_left:{keywords:["blue-square","point","direction","diagonal","northwest"],char:"↖️",fitzpatrick_scale:false,category:"symbols"},arrow_up_down:{keywords:["blue-square","direction","way","vertical"],char:"↕️",fitzpatrick_scale:false,category:"symbols"},left_right_arrow:{keywords:["shape","direction","horizontal","sideways"],char:"↔️",fitzpatrick_scale:false,category:"symbols"},arrows_counterclockwise:{keywords:["blue-square","sync","cycle"],char:"🔄",fitzpatrick_scale:false,category:"symbols"},arrow_right_hook:{keywords:["blue-square","return","rotate","direction"],char:"↪️",fitzpatrick_scale:false,category:"symbols"},leftwards_arrow_with_hook:{keywords:["back","return","blue-square","undo","enter"],char:"↩️",fitzpatrick_scale:false,category:"symbols"},arrow_heading_up:{keywords:["blue-square","direction","top"],char:"⤴️",fitzpatrick_scale:false,category:"symbols"},arrow_heading_down:{keywords:["blue-square","direction","bottom"],char:"⤵️",fitzpatrick_scale:false,category:"symbols"},hash:{keywords:["symbol","blue-square","twitter"],char:"#️⃣",fitzpatrick_scale:false,category:"symbols"},information_source:{keywords:["blue-square","alphabet","letter"],char:"ℹ️",fitzpatrick_scale:false,category:"symbols"},abc:{keywords:["blue-square","alphabet"],char:"🔤",fitzpatrick_scale:false,category:"symbols"},abcd:{keywords:["blue-square","alphabet"],char:"🔡",fitzpatrick_scale:false,category:"symbols"},capital_abcd:{keywords:["alphabet","words","blue-square"],char:"🔠",fitzpatrick_scale:false,category:"symbols"},symbols:{keywords:["blue-square","music","note","ampersand","percent","glyphs","characters"],char:"🔣",fitzpatrick_scale:false,category:"symbols"},musical_note:{keywords:["score","tone","sound"],char:"🎵",fitzpatrick_scale:false,category:"symbols"},notes:{keywords:["music","score"],char:"🎶",fitzpatrick_scale:false,category:"symbols"},wavy_dash:{keywords:["draw","line","moustache","mustache","squiggle","scribble"],char:"〰️",fitzpatrick_scale:false,category:"symbols"},curly_loop:{keywords:["scribble","draw","shape","squiggle"],char:"➰",fitzpatrick_scale:false,category:"symbols"},heavy_check_mark:{keywords:["ok","nike","answer","yes","tick"],char:"✔️",fitzpatrick_scale:false,category:"symbols"},arrows_clockwise:{keywords:["sync","cycle","round","repeat"],char:"🔃",fitzpatrick_scale:false,category:"symbols"},heavy_plus_sign:{keywords:["math","calculation","addition","more","increase"],char:"➕",fitzpatrick_scale:false,category:"symbols"},heavy_minus_sign:{keywords:["math","calculation","subtract","less"],char:"➖",fitzpatrick_scale:false,category:"symbols"},heavy_division_sign:{keywords:["divide","math","calculation"],char:"➗",fitzpatrick_scale:false,category:"symbols"},heavy_multiplication_x:{keywords:["math","calculation"],char:"✖️",fitzpatrick_scale:false,category:"symbols"},infinity:{keywords:["forever"],char:"♾",fitzpatrick_scale:false,category:"symbols"},heavy_dollar_sign:{keywords:["money","sales","payment","currency","buck"],char:"💲",fitzpatrick_scale:false,category:"symbols"},currency_exchange:{keywords:["money","sales","dollar","travel"],char:"💱",fitzpatrick_scale:false,category:"symbols"},copyright:{keywords:["ip","license","circle","law","legal"],char:"©️",fitzpatrick_scale:false,category:"symbols"},registered:{keywords:["alphabet","circle"],char:"®️",fitzpatrick_scale:false,category:"symbols"},tm:{keywords:["trademark","brand","law","legal"],char:"™️",fitzpatrick_scale:false,category:"symbols"},end:{keywords:["words","arrow"],char:"🔚",fitzpatrick_scale:false,category:"symbols"},back:{keywords:["arrow","words","return"],char:"🔙",fitzpatrick_scale:false,category:"symbols"},on:{keywords:["arrow","words"],char:"🔛",fitzpatrick_scale:false,category:"symbols"},top:{keywords:["words","blue-square"],char:"🔝",fitzpatrick_scale:false,category:"symbols"},soon:{keywords:["arrow","words"],char:"🔜",fitzpatrick_scale:false,category:"symbols"},ballot_box_with_check:{keywords:["ok","agree","confirm","black-square","vote","election","yes","tick"],char:"☑️",fitzpatrick_scale:false,category:"symbols"},radio_button:{keywords:["input","old","music","circle"],char:"🔘",fitzpatrick_scale:false,category:"symbols"},white_circle:{keywords:["shape","round"],char:"⚪",fitzpatrick_scale:false,category:"symbols"},black_circle:{keywords:["shape","button","round"],char:"⚫",fitzpatrick_scale:false,category:"symbols"},red_circle:{keywords:["shape","error","danger"],char:"🔴",fitzpatrick_scale:false,category:"symbols"},large_blue_circle:{keywords:["shape","icon","button"],char:"🔵",fitzpatrick_scale:false,category:"symbols"},small_orange_diamond:{keywords:["shape","jewel","gem"],char:"🔸",fitzpatrick_scale:false,category:"symbols"},small_blue_diamond:{keywords:["shape","jewel","gem"],char:"🔹",fitzpatrick_scale:false,category:"symbols"},large_orange_diamond:{keywords:["shape","jewel","gem"],char:"🔶",fitzpatrick_scale:false,category:"symbols"},large_blue_diamond:{keywords:["shape","jewel","gem"],char:"🔷",fitzpatrick_scale:false,category:"symbols"},small_red_triangle:{keywords:["shape","direction","up","top"],char:"🔺",fitzpatrick_scale:false,category:"symbols"},black_small_square:{keywords:["shape","icon"],char:"▪️",fitzpatrick_scale:false,category:"symbols"},white_small_square:{keywords:["shape","icon"],char:"▫️",fitzpatrick_scale:false,category:"symbols"},black_large_square:{keywords:["shape","icon","button"],char:"⬛",fitzpatrick_scale:false,category:"symbols"},white_large_square:{keywords:["shape","icon","stone","button"],char:"⬜",fitzpatrick_scale:false,category:"symbols"},small_red_triangle_down:{keywords:["shape","direction","bottom"],char:"🔻",fitzpatrick_scale:false,category:"symbols"},black_medium_square:{keywords:["shape","button","icon"],char:"◼️",fitzpatrick_scale:false,category:"symbols"},white_medium_square:{keywords:["shape","stone","icon"],char:"◻️",fitzpatrick_scale:false,category:"symbols"},black_medium_small_square:{keywords:["icon","shape","button"],char:"◾",fitzpatrick_scale:false,category:"symbols"},white_medium_small_square:{keywords:["shape","stone","icon","button"],char:"◽",fitzpatrick_scale:false,category:"symbols"},black_square_button:{keywords:["shape","input","frame"],char:"🔲",fitzpatrick_scale:false,category:"symbols"},white_square_button:{keywords:["shape","input"],char:"🔳",fitzpatrick_scale:false,category:"symbols"},speaker:{keywords:["sound","volume","silence","broadcast"],char:"🔈",fitzpatrick_scale:false,category:"symbols"},sound:{keywords:["volume","speaker","broadcast"],char:"🔉",fitzpatrick_scale:false,category:"symbols"},loud_sound:{keywords:["volume","noise","noisy","speaker","broadcast"],char:"🔊",fitzpatrick_scale:false,category:"symbols"},mute:{keywords:["sound","volume","silence","quiet"],char:"🔇",fitzpatrick_scale:false,category:"symbols"},mega:{keywords:["sound","speaker","volume"],char:"📣",fitzpatrick_scale:false,category:"symbols"},loudspeaker:{keywords:["volume","sound"],char:"📢",fitzpatrick_scale:false,category:"symbols"},bell:{keywords:["sound","notification","christmas","xmas","chime"],char:"🔔",fitzpatrick_scale:false,category:"symbols"},no_bell:{keywords:["sound","volume","mute","quiet","silent"],char:"🔕",fitzpatrick_scale:false,category:"symbols"},black_joker:{keywords:["poker","cards","game","play","magic"],char:"🃏",fitzpatrick_scale:false,category:"symbols"},mahjong:{keywords:["game","play","chinese","kanji"],char:"🀄",fitzpatrick_scale:false,category:"symbols"},spades:{keywords:["poker","cards","suits","magic"],char:"♠️",fitzpatrick_scale:false,category:"symbols"},clubs:{keywords:["poker","cards","magic","suits"],char:"♣️",fitzpatrick_scale:false,category:"symbols"},hearts:{keywords:["poker","cards","magic","suits"],char:"♥️",fitzpatrick_scale:false,category:"symbols"},diamonds:{keywords:["poker","cards","magic","suits"],char:"♦️",fitzpatrick_scale:false,category:"symbols"},flower_playing_cards:{keywords:["game","sunset","red"],char:"🎴",fitzpatrick_scale:false,category:"symbols"},thought_balloon:{keywords:["bubble","cloud","speech","thinking","dream"],char:"💭",fitzpatrick_scale:false,category:"symbols"},right_anger_bubble:{keywords:["caption","speech","thinking","mad"],char:"🗯",fitzpatrick_scale:false,category:"symbols"},speech_balloon:{keywords:["bubble","words","message","talk","chatting"],char:"💬",fitzpatrick_scale:false,category:"symbols"},left_speech_bubble:{keywords:["words","message","talk","chatting"],char:"🗨",fitzpatrick_scale:false,category:"symbols"},clock1:{keywords:["time","late","early","schedule"],char:"🕐",fitzpatrick_scale:false,category:"symbols"},clock2:{keywords:["time","late","early","schedule"],char:"🕑",fitzpatrick_scale:false,category:"symbols"},clock3:{keywords:["time","late","early","schedule"],char:"🕒",fitzpatrick_scale:false,category:"symbols"},clock4:{keywords:["time","late","early","schedule"],char:"🕓",fitzpatrick_scale:false,category:"symbols"},clock5:{keywords:["time","late","early","schedule"],char:"🕔",fitzpatrick_scale:false,category:"symbols"},clock6:{keywords:["time","late","early","schedule","dawn","dusk"],char:"🕕",fitzpatrick_scale:false,category:"symbols"},clock7:{keywords:["time","late","early","schedule"],char:"🕖",fitzpatrick_scale:false,category:"symbols"},clock8:{keywords:["time","late","early","schedule"],char:"🕗",fitzpatrick_scale:false,category:"symbols"},clock9:{keywords:["time","late","early","schedule"],char:"🕘",fitzpatrick_scale:false,category:"symbols"},clock10:{keywords:["time","late","early","schedule"],char:"🕙",fitzpatrick_scale:false,category:"symbols"},clock11:{keywords:["time","late","early","schedule"],char:"🕚",fitzpatrick_scale:false,category:"symbols"},clock12:{keywords:["time","noon","midnight","midday","late","early","schedule"],char:"🕛",fitzpatrick_scale:false,category:"symbols"},clock130:{keywords:["time","late","early","schedule"],char:"🕜",fitzpatrick_scale:false,category:"symbols"},clock230:{keywords:["time","late","early","schedule"],char:"🕝",fitzpatrick_scale:false,category:"symbols"},clock330:{keywords:["time","late","early","schedule"],char:"🕞",fitzpatrick_scale:false,category:"symbols"},clock430:{keywords:["time","late","early","schedule"],char:"🕟",fitzpatrick_scale:false,category:"symbols"},clock530:{keywords:["time","late","early","schedule"],char:"🕠",fitzpatrick_scale:false,category:"symbols"},clock630:{keywords:["time","late","early","schedule"],char:"🕡",fitzpatrick_scale:false,category:"symbols"},clock730:{keywords:["time","late","early","schedule"],char:"🕢",fitzpatrick_scale:false,category:"symbols"},clock830:{keywords:["time","late","early","schedule"],char:"🕣",fitzpatrick_scale:false,category:"symbols"},clock930:{keywords:["time","late","early","schedule"],char:"🕤",fitzpatrick_scale:false,category:"symbols"},clock1030:{keywords:["time","late","early","schedule"],char:"🕥",fitzpatrick_scale:false,category:"symbols"},clock1130:{keywords:["time","late","early","schedule"],char:"🕦",fitzpatrick_scale:false,category:"symbols"},clock1230:{keywords:["time","late","early","schedule"],char:"🕧",fitzpatrick_scale:false,category:"symbols"},afghanistan:{keywords:["af","flag","nation","country","banner"],char:"🇦🇫",fitzpatrick_scale:false,category:"flags"},aland_islands:{keywords:["Åland","islands","flag","nation","country","banner"],char:"🇦🇽",fitzpatrick_scale:false,category:"flags"},albania:{keywords:["al","flag","nation","country","banner"],char:"🇦🇱",fitzpatrick_scale:false,category:"flags"},algeria:{keywords:["dz","flag","nation","country","banner"],char:"🇩🇿",fitzpatrick_scale:false,category:"flags"},american_samoa:{keywords:["american","ws","flag","nation","country","banner"],char:"🇦🇸",fitzpatrick_scale:false,category:"flags"},andorra:{keywords:["ad","flag","nation","country","banner"],char:"🇦🇩",fitzpatrick_scale:false,category:"flags"},angola:{keywords:["ao","flag","nation","country","banner"],char:"🇦🇴",fitzpatrick_scale:false,category:"flags"},anguilla:{keywords:["ai","flag","nation","country","banner"],char:"🇦🇮",fitzpatrick_scale:false,category:"flags"},antarctica:{keywords:["aq","flag","nation","country","banner"],char:"🇦🇶",fitzpatrick_scale:false,category:"flags"},antigua_barbuda:{keywords:["antigua","barbuda","flag","nation","country","banner"],char:"🇦🇬",fitzpatrick_scale:false,category:"flags"},argentina:{keywords:["ar","flag","nation","country","banner"],char:"🇦🇷",fitzpatrick_scale:false,category:"flags"},armenia:{keywords:["am","flag","nation","country","banner"],char:"🇦🇲",fitzpatrick_scale:false,category:"flags"},aruba:{keywords:["aw","flag","nation","country","banner"],char:"🇦🇼",fitzpatrick_scale:false,category:"flags"},australia:{keywords:["au","flag","nation","country","banner"],char:"🇦🇺",fitzpatrick_scale:false,category:"flags"},austria:{keywords:["at","flag","nation","country","banner"],char:"🇦🇹",fitzpatrick_scale:false,category:"flags"},azerbaijan:{keywords:["az","flag","nation","country","banner"],char:"🇦🇿",fitzpatrick_scale:false,category:"flags"},bahamas:{keywords:["bs","flag","nation","country","banner"],char:"🇧🇸",fitzpatrick_scale:false,category:"flags"},bahrain:{keywords:["bh","flag","nation","country","banner"],char:"🇧🇭",fitzpatrick_scale:false,category:"flags"},bangladesh:{keywords:["bd","flag","nation","country","banner"],char:"🇧🇩",fitzpatrick_scale:false,category:"flags"},barbados:{keywords:["bb","flag","nation","country","banner"],char:"🇧🇧",fitzpatrick_scale:false,category:"flags"},belarus:{keywords:["by","flag","nation","country","banner"],char:"🇧🇾",fitzpatrick_scale:false,category:"flags"},belgium:{keywords:["be","flag","nation","country","banner"],char:"🇧🇪",fitzpatrick_scale:false,category:"flags"},belize:{keywords:["bz","flag","nation","country","banner"],char:"🇧🇿",fitzpatrick_scale:false,category:"flags"},benin:{keywords:["bj","flag","nation","country","banner"],char:"🇧🇯",fitzpatrick_scale:false,category:"flags"},bermuda:{keywords:["bm","flag","nation","country","banner"],char:"🇧🇲",fitzpatrick_scale:false,category:"flags"},bhutan:{keywords:["bt","flag","nation","country","banner"],char:"🇧🇹",fitzpatrick_scale:false,category:"flags"},bolivia:{keywords:["bo","flag","nation","country","banner"],char:"🇧🇴",fitzpatrick_scale:false,category:"flags"},caribbean_netherlands:{keywords:["bonaire","flag","nation","country","banner"],char:"🇧🇶",fitzpatrick_scale:false,category:"flags"},bosnia_herzegovina:{keywords:["bosnia","herzegovina","flag","nation","country","banner"],char:"🇧🇦",fitzpatrick_scale:false,category:"flags"},botswana:{keywords:["bw","flag","nation","country","banner"],char:"🇧🇼",fitzpatrick_scale:false,category:"flags"},brazil:{keywords:["br","flag","nation","country","banner"],char:"🇧🇷",fitzpatrick_scale:false,category:"flags"},british_indian_ocean_territory:{keywords:["british","indian","ocean","territory","flag","nation","country","banner"],char:"🇮🇴",fitzpatrick_scale:false,category:"flags"},british_virgin_islands:{keywords:["british","virgin","islands","bvi","flag","nation","country","banner"],char:"🇻🇬",fitzpatrick_scale:false,category:"flags"},brunei:{keywords:["bn","darussalam","flag","nation","country","banner"],char:"🇧🇳",fitzpatrick_scale:false,category:"flags"},bulgaria:{keywords:["bg","flag","nation","country","banner"],char:"🇧🇬",fitzpatrick_scale:false,category:"flags"},burkina_faso:{keywords:["burkina","faso","flag","nation","country","banner"],char:"🇧🇫",fitzpatrick_scale:false,category:"flags"},burundi:{keywords:["bi","flag","nation","country","banner"],char:"🇧🇮",fitzpatrick_scale:false,category:"flags"},cape_verde:{keywords:["cabo","verde","flag","nation","country","banner"],char:"🇨🇻",fitzpatrick_scale:false,category:"flags"},cambodia:{keywords:["kh","flag","nation","country","banner"],char:"🇰🇭",fitzpatrick_scale:false,category:"flags"},cameroon:{keywords:["cm","flag","nation","country","banner"],char:"🇨🇲",fitzpatrick_scale:false,category:"flags"},canada:{keywords:["ca","flag","nation","country","banner"],char:"🇨🇦",fitzpatrick_scale:false,category:"flags"},canary_islands:{keywords:["canary","islands","flag","nation","country","banner"],char:"🇮🇨",fitzpatrick_scale:false,category:"flags"},cayman_islands:{keywords:["cayman","islands","flag","nation","country","banner"],char:"🇰🇾",fitzpatrick_scale:false,category:"flags"},central_african_republic:{keywords:["central","african","republic","flag","nation","country","banner"],char:"🇨🇫",fitzpatrick_scale:false,category:"flags"},chad:{keywords:["td","flag","nation","country","banner"],char:"🇹🇩",fitzpatrick_scale:false,category:"flags"},chile:{keywords:["flag","nation","country","banner"],char:"🇨🇱",fitzpatrick_scale:false,category:"flags"},cn:{keywords:["china","chinese","prc","flag","country","nation","banner"],char:"🇨🇳",fitzpatrick_scale:false,category:"flags"},christmas_island:{keywords:["christmas","island","flag","nation","country","banner"],char:"🇨🇽",fitzpatrick_scale:false,category:"flags"},cocos_islands:{keywords:["cocos","keeling","islands","flag","nation","country","banner"],char:"🇨🇨",fitzpatrick_scale:false,category:"flags"},colombia:{keywords:["co","flag","nation","country","banner"],char:"🇨🇴",fitzpatrick_scale:false,category:"flags"},comoros:{keywords:["km","flag","nation","country","banner"],char:"🇰🇲",fitzpatrick_scale:false,category:"flags"},congo_brazzaville:{keywords:["congo","flag","nation","country","banner"],char:"🇨🇬",fitzpatrick_scale:false,category:"flags"},congo_kinshasa:{keywords:["congo","democratic","republic","flag","nation","country","banner"],char:"🇨🇩",fitzpatrick_scale:false,category:"flags"},cook_islands:{keywords:["cook","islands","flag","nation","country","banner"],char:"🇨🇰",fitzpatrick_scale:false,category:"flags"},costa_rica:{keywords:["costa","rica","flag","nation","country","banner"],char:"🇨🇷",fitzpatrick_scale:false,category:"flags"},croatia:{keywords:["hr","flag","nation","country","banner"],char:"🇭🇷",fitzpatrick_scale:false,category:"flags"},cuba:{keywords:["cu","flag","nation","country","banner"],char:"🇨🇺",fitzpatrick_scale:false,category:"flags"},curacao:{keywords:["curaçao","flag","nation","country","banner"],char:"🇨🇼",fitzpatrick_scale:false,category:"flags"},cyprus:{keywords:["cy","flag","nation","country","banner"],char:"🇨🇾",fitzpatrick_scale:false,category:"flags"},czech_republic:{keywords:["cz","flag","nation","country","banner"],char:"🇨🇿",fitzpatrick_scale:false,category:"flags"},denmark:{keywords:["dk","flag","nation","country","banner"],char:"🇩🇰",fitzpatrick_scale:false,category:"flags"},djibouti:{keywords:["dj","flag","nation","country","banner"],char:"🇩🇯",fitzpatrick_scale:false,category:"flags"},dominica:{keywords:["dm","flag","nation","country","banner"],char:"🇩🇲",fitzpatrick_scale:false,category:"flags"},dominican_republic:{keywords:["dominican","republic","flag","nation","country","banner"],char:"🇩🇴",fitzpatrick_scale:false,category:"flags"},ecuador:{keywords:["ec","flag","nation","country","banner"],char:"🇪🇨",fitzpatrick_scale:false,category:"flags"},egypt:{keywords:["eg","flag","nation","country","banner"],char:"🇪🇬",fitzpatrick_scale:false,category:"flags"},el_salvador:{keywords:["el","salvador","flag","nation","country","banner"],char:"🇸🇻",fitzpatrick_scale:false,category:"flags"},equatorial_guinea:{keywords:["equatorial","gn","flag","nation","country","banner"],char:"🇬🇶",fitzpatrick_scale:false,category:"flags"},eritrea:{keywords:["er","flag","nation","country","banner"],char:"🇪🇷",fitzpatrick_scale:false,category:"flags"},estonia:{keywords:["ee","flag","nation","country","banner"],char:"🇪🇪",fitzpatrick_scale:false,category:"flags"},ethiopia:{keywords:["et","flag","nation","country","banner"],char:"🇪🇹",fitzpatrick_scale:false,category:"flags"},eu:{keywords:["european","union","flag","banner"],char:"🇪🇺",fitzpatrick_scale:false,category:"flags"},falkland_islands:{keywords:["falkland","islands","malvinas","flag","nation","country","banner"],char:"🇫🇰",fitzpatrick_scale:false,category:"flags"},faroe_islands:{keywords:["faroe","islands","flag","nation","country","banner"],char:"🇫🇴",fitzpatrick_scale:false,category:"flags"},fiji:{keywords:["fj","flag","nation","country","banner"],char:"🇫🇯",fitzpatrick_scale:false,category:"flags"},finland:{keywords:["fi","flag","nation","country","banner"],char:"🇫🇮",fitzpatrick_scale:false,category:"flags"},fr:{keywords:["banner","flag","nation","france","french","country"],char:"🇫🇷",fitzpatrick_scale:false,category:"flags"},french_guiana:{keywords:["french","guiana","flag","nation","country","banner"],char:"🇬🇫",fitzpatrick_scale:false,category:"flags"},french_polynesia:{keywords:["french","polynesia","flag","nation","country","banner"],char:"🇵🇫",fitzpatrick_scale:false,category:"flags"},french_southern_territories:{keywords:["french","southern","territories","flag","nation","country","banner"],char:"🇹🇫",fitzpatrick_scale:false,category:"flags"},gabon:{keywords:["ga","flag","nation","country","banner"],char:"🇬🇦",fitzpatrick_scale:false,category:"flags"},gambia:{keywords:["gm","flag","nation","country","banner"],char:"🇬🇲",fitzpatrick_scale:false,category:"flags"},georgia:{keywords:["ge","flag","nation","country","banner"],char:"🇬🇪",fitzpatrick_scale:false,category:"flags"},de:{keywords:["german","nation","flag","country","banner"],char:"🇩🇪",fitzpatrick_scale:false,category:"flags"},ghana:{keywords:["gh","flag","nation","country","banner"],char:"🇬🇭",fitzpatrick_scale:false,category:"flags"},gibraltar:{keywords:["gi","flag","nation","country","banner"],char:"🇬🇮",fitzpatrick_scale:false,category:"flags"},greece:{keywords:["gr","flag","nation","country","banner"],char:"🇬🇷",fitzpatrick_scale:false,category:"flags"},greenland:{keywords:["gl","flag","nation","country","banner"],char:"🇬🇱",fitzpatrick_scale:false,category:"flags"},grenada:{keywords:["gd","flag","nation","country","banner"],char:"🇬🇩",fitzpatrick_scale:false,category:"flags"},guadeloupe:{keywords:["gp","flag","nation","country","banner"],char:"🇬🇵",fitzpatrick_scale:false,category:"flags"},guam:{keywords:["gu","flag","nation","country","banner"],char:"🇬🇺",fitzpatrick_scale:false,category:"flags"},guatemala:{keywords:["gt","flag","nation","country","banner"],char:"🇬🇹",fitzpatrick_scale:false,category:"flags"},guernsey:{keywords:["gg","flag","nation","country","banner"],char:"🇬🇬",fitzpatrick_scale:false,category:"flags"},guinea:{keywords:["gn","flag","nation","country","banner"],char:"🇬🇳",fitzpatrick_scale:false,category:"flags"},guinea_bissau:{keywords:["gw","bissau","flag","nation","country","banner"],char:"🇬🇼",fitzpatrick_scale:false,category:"flags"},guyana:{keywords:["gy","flag","nation","country","banner"],char:"🇬🇾",fitzpatrick_scale:false,category:"flags"},haiti:{keywords:["ht","flag","nation","country","banner"],char:"🇭🇹",fitzpatrick_scale:false,category:"flags"},honduras:{keywords:["hn","flag","nation","country","banner"],char:"🇭🇳",fitzpatrick_scale:false,category:"flags"},hong_kong:{keywords:["hong","kong","flag","nation","country","banner"],char:"🇭🇰",fitzpatrick_scale:false,category:"flags"},hungary:{keywords:["hu","flag","nation","country","banner"],char:"🇭🇺",fitzpatrick_scale:false,category:"flags"},iceland:{keywords:["is","flag","nation","country","banner"],char:"🇮🇸",fitzpatrick_scale:false,category:"flags"},india:{keywords:["in","flag","nation","country","banner"],char:"🇮🇳",fitzpatrick_scale:false,category:"flags"},indonesia:{keywords:["flag","nation","country","banner"],char:"🇮🇩",fitzpatrick_scale:false,category:"flags"},iran:{keywords:["iran,","islamic","republic","flag","nation","country","banner"],char:"🇮🇷",fitzpatrick_scale:false,category:"flags"},iraq:{keywords:["iq","flag","nation","country","banner"],char:"🇮🇶",fitzpatrick_scale:false,category:"flags"},ireland:{keywords:["ie","flag","nation","country","banner"],char:"🇮🇪",fitzpatrick_scale:false,category:"flags"},isle_of_man:{keywords:["isle","man","flag","nation","country","banner"],char:"🇮🇲",fitzpatrick_scale:false,category:"flags"},israel:{keywords:["il","flag","nation","country","banner"],char:"🇮🇱",fitzpatrick_scale:false,category:"flags"},it:{keywords:["italy","flag","nation","country","banner"],char:"🇮🇹",fitzpatrick_scale:false,category:"flags"},cote_divoire:{keywords:["ivory","coast","flag","nation","country","banner"],char:"🇨🇮",fitzpatrick_scale:false,category:"flags"},jamaica:{keywords:["jm","flag","nation","country","banner"],char:"🇯🇲",fitzpatrick_scale:false,category:"flags"},jp:{keywords:["japanese","nation","flag","country","banner"],char:"🇯🇵",fitzpatrick_scale:false,category:"flags"},jersey:{keywords:["je","flag","nation","country","banner"],char:"🇯🇪",fitzpatrick_scale:false,category:"flags"},jordan:{keywords:["jo","flag","nation","country","banner"],char:"🇯🇴",fitzpatrick_scale:false,category:"flags"},kazakhstan:{keywords:["kz","flag","nation","country","banner"],char:"🇰🇿",fitzpatrick_scale:false,category:"flags"},kenya:{keywords:["ke","flag","nation","country","banner"],char:"🇰🇪",fitzpatrick_scale:false,category:"flags"},kiribati:{keywords:["ki","flag","nation","country","banner"],char:"🇰🇮",fitzpatrick_scale:false,category:"flags"},kosovo:{keywords:["xk","flag","nation","country","banner"],char:"🇽🇰",fitzpatrick_scale:false,category:"flags"},kuwait:{keywords:["kw","flag","nation","country","banner"],char:"🇰🇼",fitzpatrick_scale:false,category:"flags"},kyrgyzstan:{keywords:["kg","flag","nation","country","banner"],char:"🇰🇬",fitzpatrick_scale:false,category:"flags"},laos:{keywords:["lao","democratic","republic","flag","nation","country","banner"],char:"🇱🇦",fitzpatrick_scale:false,category:"flags"},latvia:{keywords:["lv","flag","nation","country","banner"],char:"🇱🇻",fitzpatrick_scale:false,category:"flags"},lebanon:{keywords:["lb","flag","nation","country","banner"],char:"🇱🇧",fitzpatrick_scale:false,category:"flags"},lesotho:{keywords:["ls","flag","nation","country","banner"],char:"🇱🇸",fitzpatrick_scale:false,category:"flags"},liberia:{keywords:["lr","flag","nation","country","banner"],char:"🇱🇷",fitzpatrick_scale:false,category:"flags"},libya:{keywords:["ly","flag","nation","country","banner"],char:"🇱🇾",fitzpatrick_scale:false,category:"flags"},liechtenstein:{keywords:["li","flag","nation","country","banner"],char:"🇱🇮",fitzpatrick_scale:false,category:"flags"},lithuania:{keywords:["lt","flag","nation","country","banner"],char:"🇱🇹",fitzpatrick_scale:false,category:"flags"},luxembourg:{keywords:["lu","flag","nation","country","banner"],char:"🇱🇺",fitzpatrick_scale:false,category:"flags"},macau:{keywords:["macao","flag","nation","country","banner"],char:"🇲🇴",fitzpatrick_scale:false,category:"flags"},macedonia:{keywords:["macedonia,","flag","nation","country","banner"],char:"🇲🇰",fitzpatrick_scale:false,category:"flags"},madagascar:{keywords:["mg","flag","nation","country","banner"],char:"🇲🇬",fitzpatrick_scale:false,category:"flags"},malawi:{keywords:["mw","flag","nation","country","banner"],char:"🇲🇼",fitzpatrick_scale:false,category:"flags"},malaysia:{keywords:["my","flag","nation","country","banner"],char:"🇲🇾",fitzpatrick_scale:false,category:"flags"},maldives:{keywords:["mv","flag","nation","country","banner"],char:"🇲🇻",fitzpatrick_scale:false,category:"flags"},mali:{keywords:["ml","flag","nation","country","banner"],char:"🇲🇱",fitzpatrick_scale:false,category:"flags"},malta:{keywords:["mt","flag","nation","country","banner"],char:"🇲🇹",fitzpatrick_scale:false,category:"flags"},marshall_islands:{keywords:["marshall","islands","flag","nation","country","banner"],char:"🇲🇭",fitzpatrick_scale:false,category:"flags"},martinique:{keywords:["mq","flag","nation","country","banner"],char:"🇲🇶",fitzpatrick_scale:false,category:"flags"},mauritania:{keywords:["mr","flag","nation","country","banner"],char:"🇲🇷",fitzpatrick_scale:false,category:"flags"},mauritius:{keywords:["mu","flag","nation","country","banner"],char:"🇲🇺",fitzpatrick_scale:false,category:"flags"},mayotte:{keywords:["yt","flag","nation","country","banner"],char:"🇾🇹",fitzpatrick_scale:false,category:"flags"},mexico:{keywords:["mx","flag","nation","country","banner"],char:"🇲🇽",fitzpatrick_scale:false,category:"flags"},micronesia:{keywords:["micronesia,","federated","states","flag","nation","country","banner"],char:"🇫🇲",fitzpatrick_scale:false,category:"flags"},moldova:{keywords:["moldova,","republic","flag","nation","country","banner"],char:"🇲🇩",fitzpatrick_scale:false,category:"flags"},monaco:{keywords:["mc","flag","nation","country","banner"],char:"🇲🇨",fitzpatrick_scale:false,category:"flags"},mongolia:{keywords:["mn","flag","nation","country","banner"],char:"🇲🇳",fitzpatrick_scale:false,category:"flags"},montenegro:{keywords:["me","flag","nation","country","banner"],char:"🇲🇪",fitzpatrick_scale:false,category:"flags"},montserrat:{keywords:["ms","flag","nation","country","banner"],char:"🇲🇸",fitzpatrick_scale:false,category:"flags"},morocco:{keywords:["ma","flag","nation","country","banner"],char:"🇲🇦",fitzpatrick_scale:false,category:"flags"},mozambique:{keywords:["mz","flag","nation","country","banner"],char:"🇲🇿",fitzpatrick_scale:false,category:"flags"},myanmar:{keywords:["mm","flag","nation","country","banner"],char:"🇲🇲",fitzpatrick_scale:false,category:"flags"},namibia:{keywords:["na","flag","nation","country","banner"],char:"🇳🇦",fitzpatrick_scale:false,category:"flags"},nauru:{keywords:["nr","flag","nation","country","banner"],char:"🇳🇷",fitzpatrick_scale:false,category:"flags"},nepal:{keywords:["np","flag","nation","country","banner"],char:"🇳🇵",fitzpatrick_scale:false,category:"flags"},netherlands:{keywords:["nl","flag","nation","country","banner"],char:"🇳🇱",fitzpatrick_scale:false,category:"flags"},new_caledonia:{keywords:["new","caledonia","flag","nation","country","banner"],char:"🇳🇨",fitzpatrick_scale:false,category:"flags"},new_zealand:{keywords:["new","zealand","flag","nation","country","banner"],char:"🇳🇿",fitzpatrick_scale:false,category:"flags"},nicaragua:{keywords:["ni","flag","nation","country","banner"],char:"🇳🇮",fitzpatrick_scale:false,category:"flags"},niger:{keywords:["ne","flag","nation","country","banner"],char:"🇳🇪",fitzpatrick_scale:false,category:"flags"},nigeria:{keywords:["flag","nation","country","banner"],char:"🇳🇬",fitzpatrick_scale:false,category:"flags"},niue:{keywords:["nu","flag","nation","country","banner"],char:"🇳🇺",fitzpatrick_scale:false,category:"flags"},norfolk_island:{keywords:["norfolk","island","flag","nation","country","banner"],char:"🇳🇫",fitzpatrick_scale:false,category:"flags"},northern_mariana_islands:{keywords:["northern","mariana","islands","flag","nation","country","banner"],char:"🇲🇵",fitzpatrick_scale:false,category:"flags"},north_korea:{keywords:["north","korea","nation","flag","country","banner"],char:"🇰🇵",fitzpatrick_scale:false,category:"flags"},norway:{keywords:["no","flag","nation","country","banner"],char:"🇳🇴",fitzpatrick_scale:false,category:"flags"},oman:{keywords:["om_symbol","flag","nation","country","banner"],char:"🇴🇲",fitzpatrick_scale:false,category:"flags"},pakistan:{keywords:["pk","flag","nation","country","banner"],char:"🇵🇰",fitzpatrick_scale:false,category:"flags"},palau:{keywords:["pw","flag","nation","country","banner"],char:"🇵🇼",fitzpatrick_scale:false,category:"flags"},palestinian_territories:{keywords:["palestine","palestinian","territories","flag","nation","country","banner"],char:"🇵🇸",fitzpatrick_scale:false,category:"flags"},panama:{keywords:["pa","flag","nation","country","banner"],char:"🇵🇦",fitzpatrick_scale:false,category:"flags"},papua_new_guinea:{keywords:["papua","new","guinea","flag","nation","country","banner"],char:"🇵🇬",fitzpatrick_scale:false,category:"flags"},paraguay:{keywords:["py","flag","nation","country","banner"],char:"🇵🇾",fitzpatrick_scale:false,category:"flags"},peru:{keywords:["pe","flag","nation","country","banner"],char:"🇵🇪",fitzpatrick_scale:false,category:"flags"},philippines:{keywords:["ph","flag","nation","country","banner"],char:"🇵🇭",fitzpatrick_scale:false,category:"flags"},pitcairn_islands:{keywords:["pitcairn","flag","nation","country","banner"],char:"🇵🇳",fitzpatrick_scale:false,category:"flags"},poland:{keywords:["pl","flag","nation","country","banner"],char:"🇵🇱",fitzpatrick_scale:false,category:"flags"},portugal:{keywords:["pt","flag","nation","country","banner"],char:"🇵🇹",fitzpatrick_scale:false,category:"flags"},puerto_rico:{keywords:["puerto","rico","flag","nation","country","banner"],char:"🇵🇷",fitzpatrick_scale:false,category:"flags"},qatar:{keywords:["qa","flag","nation","country","banner"],char:"🇶🇦",fitzpatrick_scale:false,category:"flags"},reunion:{keywords:["réunion","flag","nation","country","banner"],char:"🇷🇪",fitzpatrick_scale:false,category:"flags"},romania:{keywords:["ro","flag","nation","country","banner"],char:"🇷🇴",fitzpatrick_scale:false,category:"flags"},ru:{keywords:["russian","federation","flag","nation","country","banner"],char:"🇷🇺",fitzpatrick_scale:false,category:"flags"},rwanda:{keywords:["rw","flag","nation","country","banner"],char:"🇷🇼",fitzpatrick_scale:false,category:"flags"},st_barthelemy:{keywords:["saint","barthélemy","flag","nation","country","banner"],char:"🇧🇱",fitzpatrick_scale:false,category:"flags"},st_helena:{keywords:["saint","helena","ascension","tristan","cunha","flag","nation","country","banner"],char:"🇸🇭",fitzpatrick_scale:false,category:"flags"},st_kitts_nevis:{keywords:["saint","kitts","nevis","flag","nation","country","banner"],char:"🇰🇳",fitzpatrick_scale:false,category:"flags"},st_lucia:{keywords:["saint","lucia","flag","nation","country","banner"],char:"🇱🇨",fitzpatrick_scale:false,category:"flags"},st_pierre_miquelon:{keywords:["saint","pierre","miquelon","flag","nation","country","banner"],char:"🇵🇲",fitzpatrick_scale:false,category:"flags"},st_vincent_grenadines:{keywords:["saint","vincent","grenadines","flag","nation","country","banner"],char:"🇻🇨",fitzpatrick_scale:false,category:"flags"},samoa:{keywords:["ws","flag","nation","country","banner"],char:"🇼🇸",fitzpatrick_scale:false,category:"flags"},san_marino:{keywords:["san","marino","flag","nation","country","banner"],char:"🇸🇲",fitzpatrick_scale:false,category:"flags"},sao_tome_principe:{keywords:["sao","tome","principe","flag","nation","country","banner"],char:"🇸🇹",fitzpatrick_scale:false,category:"flags"},saudi_arabia:{keywords:["flag","nation","country","banner"],char:"🇸🇦",fitzpatrick_scale:false,category:"flags"},senegal:{keywords:["sn","flag","nation","country","banner"],char:"🇸🇳",fitzpatrick_scale:false,category:"flags"},serbia:{keywords:["rs","flag","nation","country","banner"],char:"🇷🇸",fitzpatrick_scale:false,category:"flags"},seychelles:{keywords:["sc","flag","nation","country","banner"],char:"🇸🇨",fitzpatrick_scale:false,category:"flags"},sierra_leone:{keywords:["sierra","leone","flag","nation","country","banner"],char:"🇸🇱",fitzpatrick_scale:false,category:"flags"},singapore:{keywords:["sg","flag","nation","country","banner"],char:"🇸🇬",fitzpatrick_scale:false,category:"flags"},sint_maarten:{keywords:["sint","maarten","dutch","flag","nation","country","banner"],char:"🇸🇽",fitzpatrick_scale:false,category:"flags"},slovakia:{keywords:["sk","flag","nation","country","banner"],char:"🇸🇰",fitzpatrick_scale:false,category:"flags"},slovenia:{keywords:["si","flag","nation","country","banner"],char:"🇸🇮",fitzpatrick_scale:false,category:"flags"},solomon_islands:{keywords:["solomon","islands","flag","nation","country","banner"],char:"🇸🇧",fitzpatrick_scale:false,category:"flags"},somalia:{keywords:["so","flag","nation","country","banner"],char:"🇸🇴",fitzpatrick_scale:false,category:"flags"},south_africa:{keywords:["south","africa","flag","nation","country","banner"],char:"🇿🇦",fitzpatrick_scale:false,category:"flags"},south_georgia_south_sandwich_islands:{keywords:["south","georgia","sandwich","islands","flag","nation","country","banner"],char:"🇬🇸",fitzpatrick_scale:false,category:"flags"},kr:{keywords:["south","korea","nation","flag","country","banner"],char:"🇰🇷",fitzpatrick_scale:false,category:"flags"},south_sudan:{keywords:["south","sd","flag","nation","country","banner"],char:"🇸🇸",fitzpatrick_scale:false,category:"flags"},es:{keywords:["spain","flag","nation","country","banner"],char:"🇪🇸",fitzpatrick_scale:false,category:"flags"},sri_lanka:{keywords:["sri","lanka","flag","nation","country","banner"],char:"🇱🇰",fitzpatrick_scale:false,category:"flags"},sudan:{keywords:["sd","flag","nation","country","banner"],char:"🇸🇩",fitzpatrick_scale:false,category:"flags"},suriname:{keywords:["sr","flag","nation","country","banner"],char:"🇸🇷",fitzpatrick_scale:false,category:"flags"},swaziland:{keywords:["sz","flag","nation","country","banner"],char:"🇸🇿",fitzpatrick_scale:false,category:"flags"},sweden:{keywords:["se","flag","nation","country","banner"],char:"🇸🇪",fitzpatrick_scale:false,category:"flags"},switzerland:{keywords:["ch","flag","nation","country","banner"],char:"🇨🇭",fitzpatrick_scale:false,category:"flags"},syria:{keywords:["syrian","arab","republic","flag","nation","country","banner"],char:"🇸🇾",fitzpatrick_scale:false,category:"flags"},taiwan:{keywords:["tw","flag","nation","country","banner"],char:"🇹🇼",fitzpatrick_scale:false,category:"flags"},tajikistan:{keywords:["tj","flag","nation","country","banner"],char:"🇹🇯",fitzpatrick_scale:false,category:"flags"},tanzania:{keywords:["tanzania,","united","republic","flag","nation","country","banner"],char:"🇹🇿",fitzpatrick_scale:false,category:"flags"},thailand:{keywords:["th","flag","nation","country","banner"],char:"🇹🇭",fitzpatrick_scale:false,category:"flags"},timor_leste:{keywords:["timor","leste","flag","nation","country","banner"],char:"🇹🇱",fitzpatrick_scale:false,category:"flags"},togo:{keywords:["tg","flag","nation","country","banner"],char:"🇹🇬",fitzpatrick_scale:false,category:"flags"},tokelau:{keywords:["tk","flag","nation","country","banner"],char:"🇹🇰",fitzpatrick_scale:false,category:"flags"},tonga:{keywords:["to","flag","nation","country","banner"],char:"🇹🇴",fitzpatrick_scale:false,category:"flags"},trinidad_tobago:{keywords:["trinidad","tobago","flag","nation","country","banner"],char:"🇹🇹",fitzpatrick_scale:false,category:"flags"},tunisia:{keywords:["tn","flag","nation","country","banner"],char:"🇹🇳",fitzpatrick_scale:false,category:"flags"},tr:{keywords:["turkey","flag","nation","country","banner"],char:"🇹🇷",fitzpatrick_scale:false,category:"flags"},turkmenistan:{keywords:["flag","nation","country","banner"],char:"🇹🇲",fitzpatrick_scale:false,category:"flags"},turks_caicos_islands:{keywords:["turks","caicos","islands","flag","nation","country","banner"],char:"🇹🇨",fitzpatrick_scale:false,category:"flags"},tuvalu:{keywords:["flag","nation","country","banner"],char:"🇹🇻",fitzpatrick_scale:false,category:"flags"},uganda:{keywords:["ug","flag","nation","country","banner"],char:"🇺🇬",fitzpatrick_scale:false,category:"flags"},ukraine:{keywords:["ua","flag","nation","country","banner"],char:"🇺🇦",fitzpatrick_scale:false,category:"flags"},united_arab_emirates:{keywords:["united","arab","emirates","flag","nation","country","banner"],char:"🇦🇪",fitzpatrick_scale:false,category:"flags"},uk:{keywords:["united","kingdom","great","britain","northern","ireland","flag","nation","country","banner","british","UK","english","england","union jack"],char:"🇬🇧",fitzpatrick_scale:false,category:"flags"},england:{keywords:["flag","english"],char:"🏴󠁧󠁢󠁥󠁮󠁧󠁿",fitzpatrick_scale:false,category:"flags"},scotland:{keywords:["flag","scottish"],char:"🏴󠁧󠁢󠁳󠁣󠁴󠁿",fitzpatrick_scale:false,category:"flags"},wales:{keywords:["flag","welsh"],char:"🏴󠁧󠁢󠁷󠁬󠁳󠁿",fitzpatrick_scale:false,category:"flags"},us:{keywords:["united","states","america","flag","nation","country","banner"],char:"🇺🇸",fitzpatrick_scale:false,category:"flags"},us_virgin_islands:{keywords:["virgin","islands","us","flag","nation","country","banner"],char:"🇻🇮",fitzpatrick_scale:false,category:"flags"},uruguay:{keywords:["uy","flag","nation","country","banner"],char:"🇺🇾",fitzpatrick_scale:false,category:"flags"},uzbekistan:{keywords:["uz","flag","nation","country","banner"],char:"🇺🇿",fitzpatrick_scale:false,category:"flags"},vanuatu:{keywords:["vu","flag","nation","country","banner"],char:"🇻🇺",fitzpatrick_scale:false,category:"flags"},vatican_city:{keywords:["vatican","city","flag","nation","country","banner"],char:"🇻🇦",fitzpatrick_scale:false,category:"flags"},venezuela:{keywords:["ve","bolivarian","republic","flag","nation","country","banner"],char:"🇻🇪",fitzpatrick_scale:false,category:"flags"},vietnam:{keywords:["viet","nam","flag","nation","country","banner"],char:"🇻🇳",fitzpatrick_scale:false,category:"flags"},wallis_futuna:{keywords:["wallis","futuna","flag","nation","country","banner"],char:"🇼🇫",fitzpatrick_scale:false,category:"flags"},western_sahara:{keywords:["western","sahara","flag","nation","country","banner"],char:"🇪🇭",fitzpatrick_scale:false,category:"flags"},yemen:{keywords:["ye","flag","nation","country","banner"],char:"🇾🇪",fitzpatrick_scale:false,category:"flags"},zambia:{keywords:["zm","flag","nation","country","banner"],char:"🇿🇲",fitzpatrick_scale:false,category:"flags"},zimbabwe:{keywords:["zw","flag","nation","country","banner"],char:"🇿🇼",fitzpatrick_scale:false,category:"flags"},united_nations:{keywords:["un","flag","banner"],char:"🇺🇳",fitzpatrick_scale:false,category:"flags"},pirate_flag:{keywords:["skull","crossbones","flag","banner"],char:"🏴‍☠️",fitzpatrick_scale:false,category:"flags"}}); ================================================ FILE: vue_acimage_web/public/tinymce/tinymce.d.ts ================================================ interface StringPathBookmark { start: string; end?: string; forward?: boolean; } interface RangeBookmark { rng: Range; forward?: boolean; } interface IdBookmark { id: string; keep?: boolean; forward?: boolean; } interface IndexBookmark { name: string; index: number; } interface PathBookmark { start: number[]; end?: number[]; isFakeCaret?: boolean; forward?: boolean; } declare type Bookmark = StringPathBookmark | RangeBookmark | IdBookmark | IndexBookmark | PathBookmark; declare type NormalizedEvent = E & { readonly type: string; readonly target: T; readonly isDefaultPrevented: () => boolean; readonly preventDefault: () => void; readonly isPropagationStopped: () => boolean; readonly stopPropagation: () => void; readonly isImmediatePropagationStopped: () => boolean; readonly stopImmediatePropagation: () => void; }; declare type MappedEvent = K extends keyof T ? T[K] : any; interface NativeEventMap { 'beforepaste': Event; 'blur': FocusEvent; 'beforeinput': InputEvent; 'click': MouseEvent; 'compositionend': Event; 'compositionstart': Event; 'compositionupdate': Event; 'contextmenu': PointerEvent; 'copy': ClipboardEvent; 'cut': ClipboardEvent; 'dblclick': MouseEvent; 'drag': DragEvent; 'dragdrop': DragEvent; 'dragend': DragEvent; 'draggesture': DragEvent; 'dragover': DragEvent; 'dragstart': DragEvent; 'drop': DragEvent; 'focus': FocusEvent; 'focusin': FocusEvent; 'focusout': FocusEvent; 'input': InputEvent; 'keydown': KeyboardEvent; 'keypress': KeyboardEvent; 'keyup': KeyboardEvent; 'mousedown': MouseEvent; 'mouseenter': MouseEvent; 'mouseleave': MouseEvent; 'mousemove': MouseEvent; 'mouseout': MouseEvent; 'mouseover': MouseEvent; 'mouseup': MouseEvent; 'paste': ClipboardEvent; 'selectionchange': Event; 'submit': Event; 'touchend': TouchEvent; 'touchmove': TouchEvent; 'touchstart': TouchEvent; 'touchcancel': TouchEvent; 'wheel': WheelEvent; } declare type EditorEvent = NormalizedEvent; interface EventDispatcherSettings { scope?: any; toggleEvent?: (name: string, state: boolean) => void | boolean; beforeFire?: (args: EditorEvent) => void; } interface EventDispatcherConstructor { readonly prototype: EventDispatcher; new (settings?: EventDispatcherSettings): EventDispatcher; isNative: (name: string) => boolean; } declare class EventDispatcher { static isNative(name: string): boolean; private readonly settings; private readonly scope; private readonly toggleEvent; private bindings; constructor(settings?: EventDispatcherSettings); fire>(name: K, args?: U): EditorEvent; dispatch>(name: K, args?: U): EditorEvent; on(name: K, callback: false | ((event: EditorEvent>) => void | boolean), prepend?: boolean, extra?: {}): this; off(name?: K, callback?: (event: EditorEvent>) => void): this; once(name: K, callback: (event: EditorEvent>) => void, prepend?: boolean): this; has(name: string): boolean; } declare const enum UndoLevelType { Fragmented = "fragmented", Complete = "complete" } interface BaseUndoLevel { type: UndoLevelType; bookmark: Bookmark | null; beforeBookmark: Bookmark | null; } interface FragmentedUndoLevel extends BaseUndoLevel { type: UndoLevelType.Fragmented; fragments: string[]; content: ''; } interface CompleteUndoLevel extends BaseUndoLevel { type: UndoLevelType.Complete; fragments: null; content: string; } declare type NewUndoLevel = CompleteUndoLevel | FragmentedUndoLevel; declare type UndoLevel = NewUndoLevel & { bookmark: Bookmark; }; interface UndoManager { data: UndoLevel[]; typing: boolean; add: (level?: Partial, event?: EditorEvent) => UndoLevel | null; dispatchChange: () => void; beforeChange: () => void; undo: () => UndoLevel | undefined; redo: () => UndoLevel | undefined; clear: () => void; reset: () => void; hasUndo: () => boolean; hasRedo: () => boolean; transact: (callback: () => void) => UndoLevel | null; ignore: (callback: () => void) => void; extra: (callback1: () => void, callback2: () => void) => void; } declare type SchemaType = 'html4' | 'html5' | 'html5-strict'; interface ElementSettings { block_elements?: string; boolean_attributes?: string; move_caret_before_on_enter_elements?: string; non_empty_elements?: string; self_closing_elements?: string; text_block_elements?: string; text_inline_elements?: string; void_elements?: string; whitespace_elements?: string; transparent_elements?: string; } interface SchemaSettings extends ElementSettings { custom_elements?: string; extended_valid_elements?: string; invalid_elements?: string; invalid_styles?: string | Record; schema?: SchemaType; valid_children?: string; valid_classes?: string | Record; valid_elements?: string; valid_styles?: string | Record; verify_html?: boolean; padd_empty_block_inline_children?: boolean; } interface Attribute { required?: boolean; defaultValue?: string; forcedValue?: string; validValues?: Record; } interface DefaultAttribute { name: string; value: string; } interface AttributePattern extends Attribute { pattern: RegExp; } interface ElementRule { attributes: Record; attributesDefault?: DefaultAttribute[]; attributesForced?: DefaultAttribute[]; attributesOrder: string[]; attributePatterns?: AttributePattern[]; attributesRequired?: string[]; paddEmpty?: boolean; removeEmpty?: boolean; removeEmptyAttrs?: boolean; paddInEmptyBlock?: boolean; } interface SchemaElement extends ElementRule { outputName?: string; parentsRequired?: string[]; pattern?: RegExp; } interface SchemaMap { [name: string]: {}; } interface SchemaRegExpMap { [name: string]: RegExp; } interface Schema { type: SchemaType; children: Record; elements: Record; getValidStyles: () => Record | undefined; getValidClasses: () => Record | undefined; getBlockElements: () => SchemaMap; getInvalidStyles: () => Record | undefined; getVoidElements: () => SchemaMap; getTextBlockElements: () => SchemaMap; getTextInlineElements: () => SchemaMap; getBoolAttrs: () => SchemaMap; getElementRule: (name: string) => SchemaElement | undefined; getSelfClosingElements: () => SchemaMap; getNonEmptyElements: () => SchemaMap; getMoveCaretBeforeOnEnterElements: () => SchemaMap; getWhitespaceElements: () => SchemaMap; getTransparentElements: () => SchemaMap; getSpecialElements: () => SchemaRegExpMap; isValidChild: (name: string, child: string) => boolean; isValid: (name: string, attr?: string) => boolean; getCustomElements: () => SchemaMap; addValidElements: (validElements: string) => void; setValidElements: (validElements: string) => void; addCustomElements: (customElements: string) => void; addValidChildren: (validChildren: any) => void; } declare type Attributes$1 = Array<{ name: string; value: string; }> & { map: Record; }; interface AstNodeConstructor { readonly prototype: AstNode; new (name: string, type: number): AstNode; create(name: string, attrs?: Record): AstNode; } declare class AstNode { static create(name: string, attrs?: Record): AstNode; name: string; type: number; attributes?: Attributes$1; value?: string; parent?: AstNode | null; firstChild?: AstNode | null; lastChild?: AstNode | null; next?: AstNode | null; prev?: AstNode | null; raw?: boolean; constructor(name: string, type: number); replace(node: AstNode): AstNode; attr(name: string, value: string | null | undefined): AstNode | undefined; attr(name: Record | undefined): AstNode | undefined; attr(name: string): string | undefined; clone(): AstNode; wrap(wrapper: AstNode): AstNode; unwrap(): void; remove(): AstNode; append(node: AstNode): AstNode; insert(node: AstNode, refNode: AstNode, before?: boolean): AstNode; getAll(name: string): AstNode[]; children(): AstNode[]; empty(): AstNode; isEmpty(elements: SchemaMap, whitespace?: SchemaMap, predicate?: (node: AstNode) => boolean): boolean; walk(prev?: boolean): AstNode | null | undefined; } declare type Content = string | AstNode; declare type ContentFormat = 'raw' | 'text' | 'html' | 'tree'; interface GetContentArgs { format: ContentFormat; get: boolean; getInner: boolean; no_events?: boolean; save?: boolean; source_view?: boolean; [key: string]: any; } interface SetContentArgs { format: string; set: boolean; content: Content; no_events?: boolean; no_selection?: boolean; paste?: boolean; load?: boolean; initial?: boolean; [key: string]: any; } interface GetSelectionContentArgs extends GetContentArgs { selection?: boolean; contextual?: boolean; } interface SetSelectionContentArgs extends SetContentArgs { content: string; selection?: boolean; } interface BlobInfoData { id?: string; name?: string; filename?: string; blob: Blob; base64: string; blobUri?: string; uri?: string; } interface BlobInfo { id: () => string; name: () => string; filename: () => string; blob: () => Blob; base64: () => string; blobUri: () => string; uri: () => string | undefined; } interface BlobCache { create: { (o: BlobInfoData): BlobInfo; (id: string, blob: Blob, base64: string, name?: string, filename?: string): BlobInfo; }; add: (blobInfo: BlobInfo) => void; get: (id: string) => BlobInfo | undefined; getByUri: (blobUri: string) => BlobInfo | undefined; getByData: (base64: string, type: string) => BlobInfo | undefined; findFirst: (predicate: (blobInfo: BlobInfo) => boolean) => BlobInfo | undefined; removeByUri: (blobUri: string) => void; destroy: () => void; } interface BlobInfoImagePair { image: HTMLImageElement; blobInfo: BlobInfo; } declare class NodeChange { private readonly editor; private lastPath; constructor(editor: Editor); nodeChanged(args?: Record): void; private isSameElementPath; } interface SelectionOverrides { showCaret: (direction: number, node: HTMLElement, before: boolean, scrollIntoView?: boolean) => Range | null; showBlockCaretContainer: (blockCaretContainer: HTMLElement) => void; hideFakeCaret: () => void; destroy: () => void; } interface Quirks { refreshContentEditable(): void; isHidden(): boolean; } declare type DecoratorData = Record; declare type Decorator = (uid: string, data: DecoratorData) => { attributes?: {}; classes?: string[]; }; declare type AnnotationListener = (state: boolean, name: string, data?: { uid: string; nodes: any[]; }) => void; declare type AnnotationListenerApi = AnnotationListener; interface AnnotatorSettings { decorate: Decorator; persistent?: boolean; } interface Annotator { register: (name: string, settings: AnnotatorSettings) => void; annotate: (name: string, data: DecoratorData) => void; annotationChanged: (name: string, f: AnnotationListenerApi) => void; remove: (name: string) => void; removeAll: (name: string) => void; getAll: (name: string) => Record; } interface GeomRect { readonly x: number; readonly y: number; readonly w: number; readonly h: number; } interface Rect { inflate: (rect: GeomRect, w: number, h: number) => GeomRect; relativePosition: (rect: GeomRect, targetRect: GeomRect, rel: string) => GeomRect; findBestRelativePosition: (rect: GeomRect, targetRect: GeomRect, constrainRect: GeomRect, rels: string[]) => string | null; intersect: (rect: GeomRect, cropRect: GeomRect) => GeomRect | null; clamp: (rect: GeomRect, clampRect: GeomRect, fixedSize?: boolean) => GeomRect; create: (x: number, y: number, w: number, h: number) => GeomRect; fromClientRect: (clientRect: DOMRect) => GeomRect; } interface NotificationManagerImpl { open: (spec: NotificationSpec, closeCallback: () => void) => NotificationApi; close: (notification: T) => void; getArgs: (notification: T) => NotificationSpec; } interface NotificationSpec { type?: 'info' | 'warning' | 'error' | 'success'; text: string; icon?: string; progressBar?: boolean; timeout?: number; closeButton?: boolean; } interface NotificationApi { close: () => void; progressBar: { value: (percent: number) => void; }; text: (text: string) => void; reposition: () => void; getEl: () => HTMLElement; settings: NotificationSpec; } interface NotificationManager { open: (spec: NotificationSpec) => NotificationApi; close: () => void; getNotifications: () => NotificationApi[]; } interface UploadFailure { message: string; remove?: boolean; } declare type ProgressFn = (percent: number) => void; declare type UploadHandler = (blobInfo: BlobInfo, progress: ProgressFn) => Promise; interface UploadResult$2 { url: string; blobInfo: BlobInfo; status: boolean; error?: UploadFailure; } interface RawPattern { start?: any; end?: any; format?: any; cmd?: any; value?: any; replacement?: any; } interface InlineBasePattern { readonly start: string; readonly end: string; } interface InlineFormatPattern extends InlineBasePattern { readonly type: 'inline-format'; readonly format: string[]; } interface InlineCmdPattern extends InlineBasePattern { readonly type: 'inline-command'; readonly cmd: string; readonly value?: any; } declare type InlinePattern = InlineFormatPattern | InlineCmdPattern; interface BlockBasePattern { readonly start: string; } interface BlockFormatPattern extends BlockBasePattern { readonly type: 'block-format'; readonly format: string; } interface BlockCmdPattern extends BlockBasePattern { readonly type: 'block-command'; readonly cmd: string; readonly value?: any; } declare type BlockPattern = BlockFormatPattern | BlockCmdPattern; declare type Pattern = InlinePattern | BlockPattern; interface DynamicPatternContext { readonly text: string; readonly block: Element; } declare type DynamicPatternsLookup = (ctx: DynamicPatternContext) => Pattern[]; declare type RawDynamicPatternsLookup = (ctx: DynamicPatternContext) => RawPattern[]; interface AlertBannerSpec { type: 'alertbanner'; level: 'info' | 'warn' | 'error' | 'success'; text: string; icon: string; url?: string; } interface ButtonSpec { type: 'button'; text: string; enabled?: boolean; primary?: boolean; name?: string; icon?: string; borderless?: boolean; buttonType?: 'primary' | 'secondary' | 'toolbar'; } interface FormComponentSpec { type: string; name: string; } interface FormComponentWithLabelSpec extends FormComponentSpec { label?: string; } interface CheckboxSpec extends FormComponentSpec { type: 'checkbox'; label: string; enabled?: boolean; } interface CollectionSpec extends FormComponentWithLabelSpec { type: 'collection'; } interface CollectionItem { value: string; text: string; icon: string; } interface ColorInputSpec extends FormComponentWithLabelSpec { type: 'colorinput'; storageKey?: string; } interface ColorPickerSpec extends FormComponentWithLabelSpec { type: 'colorpicker'; } interface CustomEditorInit { setValue: (value: string) => void; getValue: () => string; destroy: () => void; } declare type CustomEditorInitFn = (elm: HTMLElement, settings: any) => Promise; interface CustomEditorOldSpec extends FormComponentSpec { type: 'customeditor'; tag?: string; init: (e: HTMLElement) => Promise; } interface CustomEditorNewSpec extends FormComponentSpec { type: 'customeditor'; tag?: string; scriptId: string; scriptUrl: string; settings?: any; } declare type CustomEditorSpec = CustomEditorOldSpec | CustomEditorNewSpec; interface DropZoneSpec extends FormComponentWithLabelSpec { type: 'dropzone'; } interface GridSpec { type: 'grid'; columns: number; items: BodyComponentSpec[]; } interface HtmlPanelSpec { type: 'htmlpanel'; html: string; presets?: 'presentation' | 'document'; } interface IframeSpec extends FormComponentWithLabelSpec { type: 'iframe'; sandboxed?: boolean; transparent?: boolean; } interface ImagePreviewSpec extends FormComponentSpec { type: 'imagepreview'; height?: string; } interface InputSpec extends FormComponentWithLabelSpec { type: 'input'; inputMode?: string; placeholder?: string; maximized?: boolean; enabled?: boolean; } interface LabelSpec { type: 'label'; label: string; items: BodyComponentSpec[]; } interface ListBoxSingleItemSpec { text: string; value: string; } interface ListBoxNestedItemSpec { text: string; items: ListBoxItemSpec[]; } declare type ListBoxItemSpec = ListBoxNestedItemSpec | ListBoxSingleItemSpec; interface ListBoxSpec extends FormComponentWithLabelSpec { type: 'listbox'; items: ListBoxItemSpec[]; disabled?: boolean; } interface PanelSpec { type: 'panel'; classes?: string[]; items: BodyComponentSpec[]; } interface SelectBoxItemSpec { text: string; value: string; } interface SelectBoxSpec extends FormComponentWithLabelSpec { type: 'selectbox'; items: SelectBoxItemSpec[]; size?: number; enabled?: boolean; } interface SizeInputSpec extends FormComponentWithLabelSpec { type: 'sizeinput'; constrain?: boolean; enabled?: boolean; } interface SliderSpec extends FormComponentSpec { type: 'slider'; label: string; min?: number; max?: number; } interface TableSpec { type: 'table'; header: string[]; cells: string[][]; } interface TextAreaSpec extends FormComponentWithLabelSpec { type: 'textarea'; placeholder?: string; maximized?: boolean; enabled?: boolean; } interface UrlInputSpec extends FormComponentWithLabelSpec { type: 'urlinput'; filetype?: 'image' | 'media' | 'file'; enabled?: boolean; } interface UrlInputData { value: string; meta: { text?: string; }; } declare type BodyComponentSpec = BarSpec | ButtonSpec | CheckboxSpec | TextAreaSpec | InputSpec | ListBoxSpec | SelectBoxSpec | SizeInputSpec | SliderSpec | IframeSpec | HtmlPanelSpec | UrlInputSpec | DropZoneSpec | ColorInputSpec | GridSpec | ColorPickerSpec | ImagePreviewSpec | AlertBannerSpec | CollectionSpec | LabelSpec | TableSpec | PanelSpec | CustomEditorSpec; interface BarSpec { type: 'bar'; items: BodyComponentSpec[]; } interface CommonMenuItemSpec { enabled?: boolean; text?: string; value?: string; meta?: Record; shortcut?: string; } interface CommonMenuItemInstanceApi { isEnabled: () => boolean; setEnabled: (state: boolean) => void; } interface DialogToggleMenuItemSpec extends CommonMenuItemSpec { type?: 'togglemenuitem'; name: string; } declare type DialogFooterMenuButtonItemSpec = DialogToggleMenuItemSpec; interface BaseDialogFooterButtonSpec { name?: string; align?: 'start' | 'end'; primary?: boolean; enabled?: boolean; icon?: string; buttonType?: 'primary' | 'secondary'; } interface DialogFooterNormalButtonSpec extends BaseDialogFooterButtonSpec { type: 'submit' | 'cancel' | 'custom'; text: string; } interface DialogFooterMenuButtonSpec extends BaseDialogFooterButtonSpec { type: 'menu'; text?: string; tooltip?: string; icon?: string; items: DialogFooterMenuButtonItemSpec[]; } declare type DialogFooterButtonSpec = DialogFooterNormalButtonSpec | DialogFooterMenuButtonSpec; interface TabSpec { name?: string; title: string; items: BodyComponentSpec[]; } interface TabPanelSpec { type: 'tabpanel'; tabs: TabSpec[]; } declare type DialogDataItem = any; declare type DialogData = Record; interface DialogInstanceApi { getData: () => T; setData: (data: Partial) => void; setEnabled: (name: string, state: boolean) => void; focus: (name: string) => void; showTab: (name: string) => void; redial: (nu: DialogSpec) => void; block: (msg: string) => void; unblock: () => void; close: () => void; } interface DialogActionDetails { name: string; value?: any; } interface DialogChangeDetails { name: keyof T; } interface DialogTabChangeDetails { newTabName: string; oldTabName: string; } declare type DialogActionHandler = (api: DialogInstanceApi, details: DialogActionDetails) => void; declare type DialogChangeHandler = (api: DialogInstanceApi, details: DialogChangeDetails) => void; declare type DialogSubmitHandler = (api: DialogInstanceApi) => void; declare type DialogCloseHandler = () => void; declare type DialogCancelHandler = (api: DialogInstanceApi) => void; declare type DialogTabChangeHandler = (api: DialogInstanceApi, details: DialogTabChangeDetails) => void; declare type DialogSize = 'normal' | 'medium' | 'large'; interface DialogSpec { title: string; size?: DialogSize; body: TabPanelSpec | PanelSpec; buttons: DialogFooterButtonSpec[]; initialData?: Partial; onAction?: DialogActionHandler; onChange?: DialogChangeHandler; onSubmit?: DialogSubmitHandler; onClose?: DialogCloseHandler; onCancel?: DialogCancelHandler; onTabChange?: DialogTabChangeHandler; } interface UrlDialogInstanceApi { block: (msg: string) => void; unblock: () => void; close: () => void; sendMessage: (msg: any) => void; } interface UrlDialogActionDetails { name: string; value?: any; } interface UrlDialogMessage { mceAction: string; [key: string]: any; } declare type UrlDialogActionHandler = (api: UrlDialogInstanceApi, actions: UrlDialogActionDetails) => void; declare type UrlDialogCloseHandler = () => void; declare type UrlDialogCancelHandler = (api: UrlDialogInstanceApi) => void; declare type UrlDialogMessageHandler = (api: UrlDialogInstanceApi, message: UrlDialogMessage) => void; interface UrlDialogFooterButtonSpec extends DialogFooterNormalButtonSpec { type: 'cancel' | 'custom'; } interface UrlDialogSpec { title: string; url: string; height?: number; width?: number; buttons?: UrlDialogFooterButtonSpec[]; onAction?: UrlDialogActionHandler; onClose?: UrlDialogCloseHandler; onCancel?: UrlDialogCancelHandler; onMessage?: UrlDialogMessageHandler; } declare type CardContainerDirection = 'vertical' | 'horizontal'; declare type CardContainerAlign = 'left' | 'right'; declare type CardContainerValign = 'top' | 'middle' | 'bottom'; interface CardContainerSpec { type: 'cardcontainer'; items: CardItemSpec[]; direction?: CardContainerDirection; align?: CardContainerAlign; valign?: CardContainerValign; } interface CardImageSpec { type: 'cardimage'; src: string; alt?: string; classes?: string[]; } interface CardTextSpec { type: 'cardtext'; text: string; name?: string; classes?: string[]; } declare type CardItemSpec = CardContainerSpec | CardImageSpec | CardTextSpec; interface CardMenuItemInstanceApi extends CommonMenuItemInstanceApi { } interface CardMenuItemSpec extends Omit { type: 'cardmenuitem'; label?: string; items: CardItemSpec[]; onSetup?: (api: CardMenuItemInstanceApi) => (api: CardMenuItemInstanceApi) => void; onAction?: (api: CardMenuItemInstanceApi) => void; } interface SeparatorMenuItemSpec { type?: 'separator'; text?: string; } declare type ColumnTypes$1 = number | 'auto'; declare type SeparatorItemSpec = SeparatorMenuItemSpec; interface AutocompleterItemSpec { type?: 'autocompleteitem'; value: string; text?: string; icon?: string; meta?: Record; } declare type AutocompleterContents = SeparatorItemSpec | AutocompleterItemSpec | CardMenuItemSpec; interface AutocompleterSpec { type?: 'autocompleter'; ch?: string; trigger?: string; minChars?: number; columns?: ColumnTypes$1; matches?: (rng: Range, text: string, pattern: string) => boolean; fetch: (pattern: string, maxResults: number, fetchOptions: Record) => Promise; onAction: (autocompleterApi: AutocompleterInstanceApi, rng: Range, value: string, meta: Record) => void; maxResults?: number; highlightOn?: string[]; } interface AutocompleterInstanceApi { hide: () => void; reload: (fetchOptions: Record) => void; } declare type ContextPosition = 'node' | 'selection' | 'line'; declare type ContextScope = 'node' | 'editor'; interface ContextBarSpec { predicate?: (elem: Element) => boolean; position?: ContextPosition; scope?: ContextScope; } interface BaseToolbarButtonSpec { enabled?: boolean; tooltip?: string; icon?: string; text?: string; onSetup?: (api: I) => (api: I) => void; } interface BaseToolbarButtonInstanceApi { isEnabled: () => boolean; setEnabled: (state: boolean) => void; } interface ToolbarButtonSpec extends BaseToolbarButtonSpec { type?: 'button'; onAction: (api: ToolbarButtonInstanceApi) => void; } interface ToolbarButtonInstanceApi extends BaseToolbarButtonInstanceApi { } interface BaseToolbarToggleButtonSpec extends BaseToolbarButtonSpec { active?: boolean; } interface BaseToolbarToggleButtonInstanceApi extends BaseToolbarButtonInstanceApi { isActive: () => boolean; setActive: (state: boolean) => void; } interface ToolbarToggleButtonSpec extends BaseToolbarToggleButtonSpec { type?: 'togglebutton'; onAction: (api: ToolbarToggleButtonInstanceApi) => void; } interface ToolbarToggleButtonInstanceApi extends BaseToolbarToggleButtonInstanceApi { } interface ContextFormLaunchButtonApi extends BaseToolbarButtonSpec { type: 'contextformbutton'; } interface ContextFormLaunchToggleButtonSpec extends BaseToolbarToggleButtonSpec { type: 'contextformtogglebutton'; } interface ContextFormButtonInstanceApi extends BaseToolbarButtonInstanceApi { } interface ContextFormToggleButtonInstanceApi extends BaseToolbarToggleButtonInstanceApi { } interface ContextFormButtonSpec extends BaseToolbarButtonSpec { type?: 'contextformbutton'; primary?: boolean; onAction: (formApi: ContextFormInstanceApi, api: ContextFormButtonInstanceApi) => void; } interface ContextFormToggleButtonSpec extends BaseToolbarToggleButtonSpec { type?: 'contextformtogglebutton'; onAction: (formApi: ContextFormInstanceApi, buttonApi: ContextFormToggleButtonInstanceApi) => void; primary?: boolean; } interface ContextFormInstanceApi { hide: () => void; getValue: () => string; } interface ContextFormSpec extends ContextBarSpec { type?: 'contextform'; initValue?: () => string; label?: string; launch?: ContextFormLaunchButtonApi | ContextFormLaunchToggleButtonSpec; commands: Array; } interface ContextToolbarSpec extends ContextBarSpec { type?: 'contexttoolbar'; items: string; } interface ChoiceMenuItemSpec extends CommonMenuItemSpec { type?: 'choiceitem'; icon?: string; } interface ChoiceMenuItemInstanceApi extends CommonMenuItemInstanceApi { isActive: () => boolean; setActive: (state: boolean) => void; } interface ContextMenuItem extends CommonMenuItemSpec { text: string; icon?: string; type?: 'item'; onAction: () => void; } interface ContextSubMenu extends CommonMenuItemSpec { type: 'submenu'; text: string; icon?: string; getSubmenuItems: () => string | Array; } declare type ContextMenuContents = string | ContextMenuItem | SeparatorMenuItemSpec | ContextSubMenu; interface ContextMenuApi { update: (element: Element) => string | Array; } interface FancyActionArgsMap { 'inserttable': { numRows: number; numColumns: number; }; 'colorswatch': { value: string; }; } interface BaseFancyMenuItemSpec { type: 'fancymenuitem'; fancytype: T; initData?: Record; onAction?: (data: FancyActionArgsMap[T]) => void; } interface InsertTableMenuItemSpec extends BaseFancyMenuItemSpec<'inserttable'> { fancytype: 'inserttable'; initData?: {}; } interface ColorSwatchMenuItemSpec extends BaseFancyMenuItemSpec<'colorswatch'> { fancytype: 'colorswatch'; initData?: { allowCustomColors?: boolean; colors?: ChoiceMenuItemSpec[]; storageKey?: string; }; } declare type FancyMenuItemSpec = InsertTableMenuItemSpec | ColorSwatchMenuItemSpec; interface MenuItemSpec extends CommonMenuItemSpec { type?: 'menuitem'; icon?: string; onSetup?: (api: MenuItemInstanceApi) => (api: MenuItemInstanceApi) => void; onAction?: (api: MenuItemInstanceApi) => void; } interface MenuItemInstanceApi extends CommonMenuItemInstanceApi { } declare type NestedMenuItemContents = string | MenuItemSpec | NestedMenuItemSpec | ToggleMenuItemSpec | SeparatorMenuItemSpec | FancyMenuItemSpec; interface NestedMenuItemSpec extends CommonMenuItemSpec { type?: 'nestedmenuitem'; icon?: string; getSubmenuItems: () => string | Array; onSetup?: (api: NestedMenuItemInstanceApi) => (api: NestedMenuItemInstanceApi) => void; } interface NestedMenuItemInstanceApi extends CommonMenuItemInstanceApi { } interface ToggleMenuItemSpec extends CommonMenuItemSpec { type?: 'togglemenuitem'; icon?: string; active?: boolean; onSetup?: (api: ToggleMenuItemInstanceApi) => void; onAction: (api: ToggleMenuItemInstanceApi) => void; } interface ToggleMenuItemInstanceApi extends CommonMenuItemInstanceApi { isActive: () => boolean; setActive: (state: boolean) => void; } type PublicDialog_d_AlertBannerSpec = AlertBannerSpec; type PublicDialog_d_BarSpec = BarSpec; type PublicDialog_d_BodyComponentSpec = BodyComponentSpec; type PublicDialog_d_ButtonSpec = ButtonSpec; type PublicDialog_d_CheckboxSpec = CheckboxSpec; type PublicDialog_d_CollectionItem = CollectionItem; type PublicDialog_d_CollectionSpec = CollectionSpec; type PublicDialog_d_ColorInputSpec = ColorInputSpec; type PublicDialog_d_ColorPickerSpec = ColorPickerSpec; type PublicDialog_d_CustomEditorSpec = CustomEditorSpec; type PublicDialog_d_CustomEditorInit = CustomEditorInit; type PublicDialog_d_CustomEditorInitFn = CustomEditorInitFn; type PublicDialog_d_DialogData = DialogData; type PublicDialog_d_DialogSize = DialogSize; type PublicDialog_d_DialogSpec = DialogSpec; type PublicDialog_d_DialogInstanceApi = DialogInstanceApi; type PublicDialog_d_DialogFooterButtonSpec = DialogFooterButtonSpec; type PublicDialog_d_DialogActionDetails = DialogActionDetails; type PublicDialog_d_DialogChangeDetails = DialogChangeDetails; type PublicDialog_d_DialogTabChangeDetails = DialogTabChangeDetails; type PublicDialog_d_DropZoneSpec = DropZoneSpec; type PublicDialog_d_GridSpec = GridSpec; type PublicDialog_d_HtmlPanelSpec = HtmlPanelSpec; type PublicDialog_d_IframeSpec = IframeSpec; type PublicDialog_d_ImagePreviewSpec = ImagePreviewSpec; type PublicDialog_d_InputSpec = InputSpec; type PublicDialog_d_LabelSpec = LabelSpec; type PublicDialog_d_ListBoxSpec = ListBoxSpec; type PublicDialog_d_ListBoxItemSpec = ListBoxItemSpec; type PublicDialog_d_ListBoxNestedItemSpec = ListBoxNestedItemSpec; type PublicDialog_d_ListBoxSingleItemSpec = ListBoxSingleItemSpec; type PublicDialog_d_PanelSpec = PanelSpec; type PublicDialog_d_SelectBoxSpec = SelectBoxSpec; type PublicDialog_d_SelectBoxItemSpec = SelectBoxItemSpec; type PublicDialog_d_SizeInputSpec = SizeInputSpec; type PublicDialog_d_SliderSpec = SliderSpec; type PublicDialog_d_TableSpec = TableSpec; type PublicDialog_d_TabSpec = TabSpec; type PublicDialog_d_TabPanelSpec = TabPanelSpec; type PublicDialog_d_TextAreaSpec = TextAreaSpec; type PublicDialog_d_UrlInputData = UrlInputData; type PublicDialog_d_UrlInputSpec = UrlInputSpec; type PublicDialog_d_UrlDialogSpec = UrlDialogSpec; type PublicDialog_d_UrlDialogFooterButtonSpec = UrlDialogFooterButtonSpec; type PublicDialog_d_UrlDialogInstanceApi = UrlDialogInstanceApi; type PublicDialog_d_UrlDialogActionDetails = UrlDialogActionDetails; type PublicDialog_d_UrlDialogMessage = UrlDialogMessage; declare namespace PublicDialog_d { export { PublicDialog_d_AlertBannerSpec as AlertBannerSpec, PublicDialog_d_BarSpec as BarSpec, PublicDialog_d_BodyComponentSpec as BodyComponentSpec, PublicDialog_d_ButtonSpec as ButtonSpec, PublicDialog_d_CheckboxSpec as CheckboxSpec, PublicDialog_d_CollectionItem as CollectionItem, PublicDialog_d_CollectionSpec as CollectionSpec, PublicDialog_d_ColorInputSpec as ColorInputSpec, PublicDialog_d_ColorPickerSpec as ColorPickerSpec, PublicDialog_d_CustomEditorSpec as CustomEditorSpec, PublicDialog_d_CustomEditorInit as CustomEditorInit, PublicDialog_d_CustomEditorInitFn as CustomEditorInitFn, PublicDialog_d_DialogData as DialogData, PublicDialog_d_DialogSize as DialogSize, PublicDialog_d_DialogSpec as DialogSpec, PublicDialog_d_DialogInstanceApi as DialogInstanceApi, PublicDialog_d_DialogFooterButtonSpec as DialogFooterButtonSpec, PublicDialog_d_DialogActionDetails as DialogActionDetails, PublicDialog_d_DialogChangeDetails as DialogChangeDetails, PublicDialog_d_DialogTabChangeDetails as DialogTabChangeDetails, PublicDialog_d_DropZoneSpec as DropZoneSpec, PublicDialog_d_GridSpec as GridSpec, PublicDialog_d_HtmlPanelSpec as HtmlPanelSpec, PublicDialog_d_IframeSpec as IframeSpec, PublicDialog_d_ImagePreviewSpec as ImagePreviewSpec, PublicDialog_d_InputSpec as InputSpec, PublicDialog_d_LabelSpec as LabelSpec, PublicDialog_d_ListBoxSpec as ListBoxSpec, PublicDialog_d_ListBoxItemSpec as ListBoxItemSpec, PublicDialog_d_ListBoxNestedItemSpec as ListBoxNestedItemSpec, PublicDialog_d_ListBoxSingleItemSpec as ListBoxSingleItemSpec, PublicDialog_d_PanelSpec as PanelSpec, PublicDialog_d_SelectBoxSpec as SelectBoxSpec, PublicDialog_d_SelectBoxItemSpec as SelectBoxItemSpec, PublicDialog_d_SizeInputSpec as SizeInputSpec, PublicDialog_d_SliderSpec as SliderSpec, PublicDialog_d_TableSpec as TableSpec, PublicDialog_d_TabSpec as TabSpec, PublicDialog_d_TabPanelSpec as TabPanelSpec, PublicDialog_d_TextAreaSpec as TextAreaSpec, PublicDialog_d_UrlInputData as UrlInputData, PublicDialog_d_UrlInputSpec as UrlInputSpec, PublicDialog_d_UrlDialogSpec as UrlDialogSpec, PublicDialog_d_UrlDialogFooterButtonSpec as UrlDialogFooterButtonSpec, PublicDialog_d_UrlDialogInstanceApi as UrlDialogInstanceApi, PublicDialog_d_UrlDialogActionDetails as UrlDialogActionDetails, PublicDialog_d_UrlDialogMessage as UrlDialogMessage, }; } type PublicInlineContent_d_AutocompleterSpec = AutocompleterSpec; type PublicInlineContent_d_AutocompleterItemSpec = AutocompleterItemSpec; type PublicInlineContent_d_AutocompleterContents = AutocompleterContents; type PublicInlineContent_d_AutocompleterInstanceApi = AutocompleterInstanceApi; type PublicInlineContent_d_ContextPosition = ContextPosition; type PublicInlineContent_d_ContextScope = ContextScope; type PublicInlineContent_d_ContextFormSpec = ContextFormSpec; type PublicInlineContent_d_ContextFormInstanceApi = ContextFormInstanceApi; type PublicInlineContent_d_ContextFormButtonSpec = ContextFormButtonSpec; type PublicInlineContent_d_ContextFormButtonInstanceApi = ContextFormButtonInstanceApi; type PublicInlineContent_d_ContextFormToggleButtonSpec = ContextFormToggleButtonSpec; type PublicInlineContent_d_ContextFormToggleButtonInstanceApi = ContextFormToggleButtonInstanceApi; type PublicInlineContent_d_ContextToolbarSpec = ContextToolbarSpec; type PublicInlineContent_d_SeparatorItemSpec = SeparatorItemSpec; declare namespace PublicInlineContent_d { export { PublicInlineContent_d_AutocompleterSpec as AutocompleterSpec, PublicInlineContent_d_AutocompleterItemSpec as AutocompleterItemSpec, PublicInlineContent_d_AutocompleterContents as AutocompleterContents, PublicInlineContent_d_AutocompleterInstanceApi as AutocompleterInstanceApi, PublicInlineContent_d_ContextPosition as ContextPosition, PublicInlineContent_d_ContextScope as ContextScope, PublicInlineContent_d_ContextFormSpec as ContextFormSpec, PublicInlineContent_d_ContextFormInstanceApi as ContextFormInstanceApi, PublicInlineContent_d_ContextFormButtonSpec as ContextFormButtonSpec, PublicInlineContent_d_ContextFormButtonInstanceApi as ContextFormButtonInstanceApi, PublicInlineContent_d_ContextFormToggleButtonSpec as ContextFormToggleButtonSpec, PublicInlineContent_d_ContextFormToggleButtonInstanceApi as ContextFormToggleButtonInstanceApi, PublicInlineContent_d_ContextToolbarSpec as ContextToolbarSpec, PublicInlineContent_d_SeparatorItemSpec as SeparatorItemSpec, }; } type PublicMenu_d_MenuItemSpec = MenuItemSpec; type PublicMenu_d_MenuItemInstanceApi = MenuItemInstanceApi; type PublicMenu_d_NestedMenuItemContents = NestedMenuItemContents; type PublicMenu_d_NestedMenuItemSpec = NestedMenuItemSpec; type PublicMenu_d_NestedMenuItemInstanceApi = NestedMenuItemInstanceApi; type PublicMenu_d_FancyMenuItemSpec = FancyMenuItemSpec; type PublicMenu_d_ColorSwatchMenuItemSpec = ColorSwatchMenuItemSpec; type PublicMenu_d_InsertTableMenuItemSpec = InsertTableMenuItemSpec; type PublicMenu_d_ToggleMenuItemSpec = ToggleMenuItemSpec; type PublicMenu_d_ToggleMenuItemInstanceApi = ToggleMenuItemInstanceApi; type PublicMenu_d_ChoiceMenuItemSpec = ChoiceMenuItemSpec; type PublicMenu_d_ChoiceMenuItemInstanceApi = ChoiceMenuItemInstanceApi; type PublicMenu_d_SeparatorMenuItemSpec = SeparatorMenuItemSpec; type PublicMenu_d_ContextMenuApi = ContextMenuApi; type PublicMenu_d_ContextMenuContents = ContextMenuContents; type PublicMenu_d_ContextMenuItem = ContextMenuItem; type PublicMenu_d_ContextSubMenu = ContextSubMenu; type PublicMenu_d_CardMenuItemSpec = CardMenuItemSpec; type PublicMenu_d_CardMenuItemInstanceApi = CardMenuItemInstanceApi; type PublicMenu_d_CardItemSpec = CardItemSpec; type PublicMenu_d_CardContainerSpec = CardContainerSpec; type PublicMenu_d_CardImageSpec = CardImageSpec; type PublicMenu_d_CardTextSpec = CardTextSpec; declare namespace PublicMenu_d { export { PublicMenu_d_MenuItemSpec as MenuItemSpec, PublicMenu_d_MenuItemInstanceApi as MenuItemInstanceApi, PublicMenu_d_NestedMenuItemContents as NestedMenuItemContents, PublicMenu_d_NestedMenuItemSpec as NestedMenuItemSpec, PublicMenu_d_NestedMenuItemInstanceApi as NestedMenuItemInstanceApi, PublicMenu_d_FancyMenuItemSpec as FancyMenuItemSpec, PublicMenu_d_ColorSwatchMenuItemSpec as ColorSwatchMenuItemSpec, PublicMenu_d_InsertTableMenuItemSpec as InsertTableMenuItemSpec, PublicMenu_d_ToggleMenuItemSpec as ToggleMenuItemSpec, PublicMenu_d_ToggleMenuItemInstanceApi as ToggleMenuItemInstanceApi, PublicMenu_d_ChoiceMenuItemSpec as ChoiceMenuItemSpec, PublicMenu_d_ChoiceMenuItemInstanceApi as ChoiceMenuItemInstanceApi, PublicMenu_d_SeparatorMenuItemSpec as SeparatorMenuItemSpec, PublicMenu_d_ContextMenuApi as ContextMenuApi, PublicMenu_d_ContextMenuContents as ContextMenuContents, PublicMenu_d_ContextMenuItem as ContextMenuItem, PublicMenu_d_ContextSubMenu as ContextSubMenu, PublicMenu_d_CardMenuItemSpec as CardMenuItemSpec, PublicMenu_d_CardMenuItemInstanceApi as CardMenuItemInstanceApi, PublicMenu_d_CardItemSpec as CardItemSpec, PublicMenu_d_CardContainerSpec as CardContainerSpec, PublicMenu_d_CardImageSpec as CardImageSpec, PublicMenu_d_CardTextSpec as CardTextSpec, }; } interface SidebarInstanceApi { element: () => HTMLElement; } interface SidebarSpec { icon?: string; tooltip?: string; onShow?: (api: SidebarInstanceApi) => void; onSetup?: (api: SidebarInstanceApi) => (api: SidebarInstanceApi) => void; onHide?: (api: SidebarInstanceApi) => void; } type PublicSidebar_d_SidebarSpec = SidebarSpec; type PublicSidebar_d_SidebarInstanceApi = SidebarInstanceApi; declare namespace PublicSidebar_d { export { PublicSidebar_d_SidebarSpec as SidebarSpec, PublicSidebar_d_SidebarInstanceApi as SidebarInstanceApi, }; } interface ToolbarGroupSetting { name: string; items: string[]; } declare type ToolbarConfig = string | ToolbarGroupSetting[]; interface GroupToolbarButtonInstanceApi extends BaseToolbarButtonInstanceApi { } interface GroupToolbarButtonSpec extends BaseToolbarButtonSpec { type?: 'grouptoolbarbutton'; items?: ToolbarConfig; } declare type MenuButtonItemTypes = NestedMenuItemContents; declare type SuccessCallback$1 = (menu: string | MenuButtonItemTypes[]) => void; interface MenuButtonFetchContext { pattern: string; } interface BaseMenuButtonSpec { text?: string; tooltip?: string; icon?: string; search?: boolean | { placeholder?: string; }; fetch: (success: SuccessCallback$1, fetchContext: MenuButtonFetchContext) => void; onSetup?: (api: BaseMenuButtonInstanceApi) => (api: BaseMenuButtonInstanceApi) => void; } interface BaseMenuButtonInstanceApi { isEnabled: () => boolean; setEnabled: (state: boolean) => void; isActive: () => boolean; setActive: (state: boolean) => void; } interface ToolbarMenuButtonSpec extends BaseMenuButtonSpec { type?: 'menubutton'; onSetup?: (api: ToolbarMenuButtonInstanceApi) => (api: ToolbarMenuButtonInstanceApi) => void; } interface ToolbarMenuButtonInstanceApi extends BaseMenuButtonInstanceApi { } declare type ToolbarSplitButtonItemTypes = ChoiceMenuItemSpec | SeparatorMenuItemSpec; declare type SuccessCallback = (menu: ToolbarSplitButtonItemTypes[]) => void; declare type SelectPredicate = (value: string) => boolean; declare type PresetTypes = 'color' | 'normal' | 'listpreview'; declare type ColumnTypes = number | 'auto'; interface ToolbarSplitButtonSpec { type?: 'splitbutton'; tooltip?: string; icon?: string; text?: string; select?: SelectPredicate; presets?: PresetTypes; columns?: ColumnTypes; fetch: (success: SuccessCallback) => void; onSetup?: (api: ToolbarSplitButtonInstanceApi) => (api: ToolbarSplitButtonInstanceApi) => void; onAction: (api: ToolbarSplitButtonInstanceApi) => void; onItemAction: (api: ToolbarSplitButtonInstanceApi, value: string) => void; } interface ToolbarSplitButtonInstanceApi { isEnabled: () => boolean; setEnabled: (state: boolean) => void; setIconFill: (id: string, value: string) => void; isActive: () => boolean; setActive: (state: boolean) => void; } type PublicToolbar_d_ToolbarButtonSpec = ToolbarButtonSpec; type PublicToolbar_d_ToolbarButtonInstanceApi = ToolbarButtonInstanceApi; type PublicToolbar_d_ToolbarSplitButtonSpec = ToolbarSplitButtonSpec; type PublicToolbar_d_ToolbarSplitButtonInstanceApi = ToolbarSplitButtonInstanceApi; type PublicToolbar_d_ToolbarMenuButtonSpec = ToolbarMenuButtonSpec; type PublicToolbar_d_ToolbarMenuButtonInstanceApi = ToolbarMenuButtonInstanceApi; type PublicToolbar_d_ToolbarToggleButtonSpec = ToolbarToggleButtonSpec; type PublicToolbar_d_ToolbarToggleButtonInstanceApi = ToolbarToggleButtonInstanceApi; type PublicToolbar_d_GroupToolbarButtonSpec = GroupToolbarButtonSpec; type PublicToolbar_d_GroupToolbarButtonInstanceApi = GroupToolbarButtonInstanceApi; declare namespace PublicToolbar_d { export { PublicToolbar_d_ToolbarButtonSpec as ToolbarButtonSpec, PublicToolbar_d_ToolbarButtonInstanceApi as ToolbarButtonInstanceApi, PublicToolbar_d_ToolbarSplitButtonSpec as ToolbarSplitButtonSpec, PublicToolbar_d_ToolbarSplitButtonInstanceApi as ToolbarSplitButtonInstanceApi, PublicToolbar_d_ToolbarMenuButtonSpec as ToolbarMenuButtonSpec, PublicToolbar_d_ToolbarMenuButtonInstanceApi as ToolbarMenuButtonInstanceApi, PublicToolbar_d_ToolbarToggleButtonSpec as ToolbarToggleButtonSpec, PublicToolbar_d_ToolbarToggleButtonInstanceApi as ToolbarToggleButtonInstanceApi, PublicToolbar_d_GroupToolbarButtonSpec as GroupToolbarButtonSpec, PublicToolbar_d_GroupToolbarButtonInstanceApi as GroupToolbarButtonInstanceApi, }; } interface ViewNormalButtonSpec { type: 'button'; text: string; buttonType?: 'primary' | 'secondary'; onAction: () => void; } declare type ViewButtonSpec = ViewNormalButtonSpec; interface ViewInstanceApi { getContainer: () => HTMLElement; } interface ViewSpec { buttons?: ViewButtonSpec[]; onShow: (api: ViewInstanceApi) => void; onHide: (api: ViewInstanceApi) => void; } type PublicView_d_ViewSpec = ViewSpec; type PublicView_d_ViewInstanceApi = ViewInstanceApi; declare namespace PublicView_d { export { PublicView_d_ViewSpec as ViewSpec, PublicView_d_ViewInstanceApi as ViewInstanceApi, }; } interface Registry$1 { addButton: (name: string, spec: ToolbarButtonSpec) => void; addGroupToolbarButton: (name: string, spec: GroupToolbarButtonSpec) => void; addToggleButton: (name: string, spec: ToolbarToggleButtonSpec) => void; addMenuButton: (name: string, spec: ToolbarMenuButtonSpec) => void; addSplitButton: (name: string, spec: ToolbarSplitButtonSpec) => void; addMenuItem: (name: string, spec: MenuItemSpec) => void; addNestedMenuItem: (name: string, spec: NestedMenuItemSpec) => void; addToggleMenuItem: (name: string, spec: ToggleMenuItemSpec) => void; addContextMenu: (name: string, spec: ContextMenuApi) => void; addContextToolbar: (name: string, spec: ContextToolbarSpec) => void; addContextForm: (name: string, spec: ContextFormSpec) => void; addIcon: (name: string, svgData: string) => void; addAutocompleter: (name: string, spec: AutocompleterSpec) => void; addSidebar: (name: string, spec: SidebarSpec) => void; addView: (name: string, spec: ViewSpec) => void; getAll: () => { buttons: Record; menuItems: Record; popups: Record; contextMenus: Record; contextToolbars: Record; icons: Record; sidebars: Record; views: Record; }; } interface AutocompleteLookupData { readonly matchText: string; readonly items: AutocompleterContents[]; readonly columns: ColumnTypes$1; readonly onAction: (autoApi: AutocompleterInstanceApi, rng: Range, value: string, meta: Record) => void; readonly highlightOn: string[]; } interface AutocompleterEventArgs { readonly lookupData: AutocompleteLookupData[]; } interface RangeLikeObject { startContainer: Node; startOffset: number; endContainer: Node; endOffset: number; } declare type ApplyFormat = BlockFormat | InlineFormat | SelectorFormat; declare type RemoveFormat = RemoveBlockFormat | RemoveInlineFormat | RemoveSelectorFormat; declare type Format = ApplyFormat | RemoveFormat; declare type Formats = Record; declare type FormatAttrOrStyleValue = string | ((vars?: FormatVars) => string | null); declare type FormatVars = Record; interface BaseFormat { ceFalseOverride?: boolean; classes?: string | string[]; collapsed?: boolean; exact?: boolean; expand?: boolean; links?: boolean; mixed?: boolean; block_expand?: boolean; onmatch?: (node: Element, fmt: T, itemName: string) => boolean; remove?: 'none' | 'empty' | 'all'; remove_similar?: boolean; split?: boolean; deep?: boolean; preserve_attributes?: string[]; } interface Block { block: string; list_block?: string; wrapper?: boolean; } interface Inline { inline: string; } interface Selector { selector: string; inherit?: boolean; } interface CommonFormat extends BaseFormat { attributes?: Record; styles?: Record; toggle?: boolean; preview?: string | false; onformat?: (elm: Element, fmt: T, vars?: FormatVars, node?: Node | RangeLikeObject | null) => void; clear_child_styles?: boolean; merge_siblings?: boolean; merge_with_parents?: boolean; } interface BlockFormat extends Block, CommonFormat { } interface InlineFormat extends Inline, CommonFormat { } interface SelectorFormat extends Selector, CommonFormat { } interface CommonRemoveFormat extends BaseFormat { attributes?: string[] | Record; styles?: string[] | Record; } interface RemoveBlockFormat extends Block, CommonRemoveFormat { } interface RemoveInlineFormat extends Inline, CommonRemoveFormat { } interface RemoveSelectorFormat extends Selector, CommonRemoveFormat { } interface Filter { name: string; callbacks: C[]; } interface ParserArgs { getInner?: boolean | number; forced_root_block?: boolean | string; context?: string; isRootContent?: boolean; format?: string; invalid?: boolean; no_events?: boolean; [key: string]: any; } declare type ParserFilterCallback = (nodes: AstNode[], name: string, args: ParserArgs) => void; interface ParserFilter extends Filter { } interface DomParserSettings { allow_html_data_urls?: boolean; allow_svg_data_urls?: boolean; allow_conditional_comments?: boolean; allow_html_in_named_anchor?: boolean; allow_script_urls?: boolean; allow_unsafe_link_target?: boolean; convert_fonts_to_spans?: boolean; fix_list_elements?: boolean; font_size_legacy_values?: string; forced_root_block?: boolean | string; forced_root_block_attrs?: Record; preserve_cdata?: boolean; remove_trailing_brs?: boolean; root_name?: string; validate?: boolean; inline_styles?: boolean; blob_cache?: BlobCache; document?: Document; } interface DomParser { schema: Schema; addAttributeFilter: (name: string, callback: ParserFilterCallback) => void; getAttributeFilters: () => ParserFilter[]; removeAttributeFilter: (name: string, callback?: ParserFilterCallback) => void; addNodeFilter: (name: string, callback: ParserFilterCallback) => void; getNodeFilters: () => ParserFilter[]; removeNodeFilter: (name: string, callback?: ParserFilterCallback) => void; parse: (html: string, args?: ParserArgs) => AstNode; } interface StyleSheetLoaderSettings { maxLoadTime?: number; contentCssCors?: boolean; referrerPolicy?: ReferrerPolicy; } interface StyleSheetLoader { load: (url: string) => Promise; loadAll: (urls: string[]) => Promise; unload: (url: string) => void; unloadAll: (urls: string[]) => void; _setReferrerPolicy: (referrerPolicy: ReferrerPolicy) => void; _setContentCssCors: (contentCssCors: boolean) => void; } declare type Registry = Registry$1; interface EditorUiApi { show: () => void; hide: () => void; setEnabled: (state: boolean) => void; isEnabled: () => boolean; } interface EditorUi extends EditorUiApi { registry: Registry; styleSheetLoader: StyleSheetLoader; } type Ui_d_Registry = Registry; type Ui_d_EditorUiApi = EditorUiApi; type Ui_d_EditorUi = EditorUi; declare namespace Ui_d { export { Ui_d_Registry as Registry, PublicDialog_d as Dialog, PublicInlineContent_d as InlineContent, PublicMenu_d as Menu, PublicView_d as View, PublicSidebar_d as Sidebar, PublicToolbar_d as Toolbar, Ui_d_EditorUiApi as EditorUiApi, Ui_d_EditorUi as EditorUi, }; } interface WindowParams { readonly inline?: 'cursor' | 'toolbar'; readonly ariaAttrs?: boolean; } declare type InstanceApi = UrlDialogInstanceApi | DialogInstanceApi; interface WindowManagerImpl { open: (config: DialogSpec, params: WindowParams | undefined, closeWindow: (dialog: DialogInstanceApi) => void) => DialogInstanceApi; openUrl: (config: UrlDialogSpec, closeWindow: (dialog: UrlDialogInstanceApi) => void) => UrlDialogInstanceApi; alert: (message: string, callback: () => void) => void; confirm: (message: string, callback: (state: boolean) => void) => void; close: (dialog: InstanceApi) => void; } interface WindowManager { open: (config: DialogSpec, params?: WindowParams) => DialogInstanceApi; openUrl: (config: UrlDialogSpec) => UrlDialogInstanceApi; alert: (message: string, callback?: () => void, scope?: any) => void; confirm: (message: string, callback?: (state: boolean) => void, scope?: any) => void; close: () => void; } interface ExecCommandEvent { command: string; ui: boolean; value?: any; } interface BeforeGetContentEvent extends GetContentArgs { selection?: boolean; } interface GetContentEvent extends BeforeGetContentEvent { content: string; } interface BeforeSetContentEvent extends SetContentArgs { content: string; selection?: boolean; } interface SetContentEvent extends BeforeSetContentEvent { content: string; } interface SaveContentEvent extends GetContentEvent { save: boolean; } interface NewBlockEvent { newBlock: Element; } interface NodeChangeEvent { element: Element; parents: Node[]; selectionChange?: boolean; initial?: boolean; } interface FormatEvent { format: string; vars?: FormatVars; node?: Node | RangeLikeObject | null; } interface ObjectResizeEvent { target: HTMLElement; width: number; height: number; origin: string; } interface ObjectSelectedEvent { target: Node; targetClone?: Node; } interface ScrollIntoViewEvent { elm: HTMLElement; alignToTop: boolean | undefined; } interface SetSelectionRangeEvent { range: Range; forward: boolean | undefined; } interface ShowCaretEvent { target: Node; direction: number; before: boolean; } interface SwitchModeEvent { mode: string; } interface ChangeEvent { level: UndoLevel; lastLevel: UndoLevel | undefined; } interface AddUndoEvent extends ChangeEvent { originalEvent: Event | undefined; } interface UndoRedoEvent { level: UndoLevel; } interface WindowEvent { dialog: InstanceApi; } interface ProgressStateEvent { state: boolean; time?: number; } interface AfterProgressStateEvent { state: boolean; } interface PlaceholderToggleEvent { state: boolean; } interface LoadErrorEvent { message: string; } interface PreProcessEvent extends ParserArgs { node: Element; } interface PostProcessEvent extends ParserArgs { content: string; } interface PastePlainTextToggleEvent { state: boolean; } interface PastePreProcessEvent { content: string; readonly internal: boolean; } interface PastePostProcessEvent { node: HTMLElement; readonly internal: boolean; } interface NewTableRowEvent { node: HTMLTableRowElement; } interface NewTableCellEvent { node: HTMLTableCellElement; } interface TableEventData { readonly structure: boolean; readonly style: boolean; } interface TableModifiedEvent extends TableEventData { readonly table: HTMLTableElement; } interface BeforeOpenNotificationEvent { notification: NotificationSpec; } interface OpenNotificationEvent { notification: NotificationApi; } interface EditorEventMap extends Omit { 'activate': { relatedTarget: Editor | null; }; 'deactivate': { relatedTarget: Editor; }; 'focus': { blurredEditor: Editor | null; }; 'blur': { focusedEditor: Editor | null; }; 'resize': UIEvent; 'scroll': UIEvent; 'detach': {}; 'remove': {}; 'init': {}; 'ScrollIntoView': ScrollIntoViewEvent; 'AfterScrollIntoView': ScrollIntoViewEvent; 'ObjectResized': ObjectResizeEvent; 'ObjectResizeStart': ObjectResizeEvent; 'SwitchMode': SwitchModeEvent; 'ScrollWindow': Event; 'ResizeWindow': UIEvent; 'SkinLoaded': {}; 'SkinLoadError': LoadErrorEvent; 'PluginLoadError': LoadErrorEvent; 'ModelLoadError': LoadErrorEvent; 'IconsLoadError': LoadErrorEvent; 'ThemeLoadError': LoadErrorEvent; 'LanguageLoadError': LoadErrorEvent; 'BeforeExecCommand': ExecCommandEvent; 'ExecCommand': ExecCommandEvent; 'NodeChange': NodeChangeEvent; 'FormatApply': FormatEvent; 'FormatRemove': FormatEvent; 'ShowCaret': ShowCaretEvent; 'SelectionChange': {}; 'ObjectSelected': ObjectSelectedEvent; 'BeforeObjectSelected': ObjectSelectedEvent; 'GetSelectionRange': { range: Range; }; 'SetSelectionRange': SetSelectionRangeEvent; 'AfterSetSelectionRange': SetSelectionRangeEvent; 'BeforeGetContent': BeforeGetContentEvent; 'GetContent': GetContentEvent; 'BeforeSetContent': BeforeSetContentEvent; 'SetContent': SetContentEvent; 'SaveContent': SaveContentEvent; 'RawSaveContent': SaveContentEvent; 'LoadContent': { load: boolean; element: HTMLElement; }; 'PreviewFormats': {}; 'AfterPreviewFormats': {}; 'ScriptsLoaded': {}; 'PreInit': {}; 'PostRender': {}; 'NewBlock': NewBlockEvent; 'ClearUndos': {}; 'TypingUndo': {}; 'Redo': UndoRedoEvent; 'Undo': UndoRedoEvent; 'BeforeAddUndo': AddUndoEvent; 'AddUndo': AddUndoEvent; 'change': ChangeEvent; 'CloseWindow': WindowEvent; 'OpenWindow': WindowEvent; 'ProgressState': ProgressStateEvent; 'AfterProgressState': AfterProgressStateEvent; 'PlaceholderToggle': PlaceholderToggleEvent; 'tap': TouchEvent; 'longpress': TouchEvent; 'longpresscancel': {}; 'PreProcess': PreProcessEvent; 'PostProcess': PostProcessEvent; 'AutocompleterStart': AutocompleterEventArgs; 'AutocompleterUpdate': AutocompleterEventArgs; 'AutocompleterEnd': {}; 'PastePlainTextToggle': PastePlainTextToggleEvent; 'PastePreProcess': PastePreProcessEvent; 'PastePostProcess': PastePostProcessEvent; 'TableModified': TableModifiedEvent; 'NewRow': NewTableRowEvent; 'NewCell': NewTableCellEvent; 'SetAttrib': SetAttribEvent; 'hide': {}; 'show': {}; 'dirty': {}; 'BeforeOpenNotification': BeforeOpenNotificationEvent; 'OpenNotification': OpenNotificationEvent; } interface EditorManagerEventMap { 'AddEditor': { editor: Editor; }; 'RemoveEditor': { editor: Editor; }; 'BeforeUnload': { returnValue: any; }; } type EventTypes_d_ExecCommandEvent = ExecCommandEvent; type EventTypes_d_BeforeGetContentEvent = BeforeGetContentEvent; type EventTypes_d_GetContentEvent = GetContentEvent; type EventTypes_d_BeforeSetContentEvent = BeforeSetContentEvent; type EventTypes_d_SetContentEvent = SetContentEvent; type EventTypes_d_SaveContentEvent = SaveContentEvent; type EventTypes_d_NewBlockEvent = NewBlockEvent; type EventTypes_d_NodeChangeEvent = NodeChangeEvent; type EventTypes_d_FormatEvent = FormatEvent; type EventTypes_d_ObjectResizeEvent = ObjectResizeEvent; type EventTypes_d_ObjectSelectedEvent = ObjectSelectedEvent; type EventTypes_d_ScrollIntoViewEvent = ScrollIntoViewEvent; type EventTypes_d_SetSelectionRangeEvent = SetSelectionRangeEvent; type EventTypes_d_ShowCaretEvent = ShowCaretEvent; type EventTypes_d_SwitchModeEvent = SwitchModeEvent; type EventTypes_d_ChangeEvent = ChangeEvent; type EventTypes_d_AddUndoEvent = AddUndoEvent; type EventTypes_d_UndoRedoEvent = UndoRedoEvent; type EventTypes_d_WindowEvent = WindowEvent; type EventTypes_d_ProgressStateEvent = ProgressStateEvent; type EventTypes_d_AfterProgressStateEvent = AfterProgressStateEvent; type EventTypes_d_PlaceholderToggleEvent = PlaceholderToggleEvent; type EventTypes_d_LoadErrorEvent = LoadErrorEvent; type EventTypes_d_PreProcessEvent = PreProcessEvent; type EventTypes_d_PostProcessEvent = PostProcessEvent; type EventTypes_d_PastePlainTextToggleEvent = PastePlainTextToggleEvent; type EventTypes_d_PastePreProcessEvent = PastePreProcessEvent; type EventTypes_d_PastePostProcessEvent = PastePostProcessEvent; type EventTypes_d_NewTableRowEvent = NewTableRowEvent; type EventTypes_d_NewTableCellEvent = NewTableCellEvent; type EventTypes_d_TableEventData = TableEventData; type EventTypes_d_TableModifiedEvent = TableModifiedEvent; type EventTypes_d_BeforeOpenNotificationEvent = BeforeOpenNotificationEvent; type EventTypes_d_OpenNotificationEvent = OpenNotificationEvent; type EventTypes_d_EditorEventMap = EditorEventMap; type EventTypes_d_EditorManagerEventMap = EditorManagerEventMap; declare namespace EventTypes_d { export { EventTypes_d_ExecCommandEvent as ExecCommandEvent, EventTypes_d_BeforeGetContentEvent as BeforeGetContentEvent, EventTypes_d_GetContentEvent as GetContentEvent, EventTypes_d_BeforeSetContentEvent as BeforeSetContentEvent, EventTypes_d_SetContentEvent as SetContentEvent, EventTypes_d_SaveContentEvent as SaveContentEvent, EventTypes_d_NewBlockEvent as NewBlockEvent, EventTypes_d_NodeChangeEvent as NodeChangeEvent, EventTypes_d_FormatEvent as FormatEvent, EventTypes_d_ObjectResizeEvent as ObjectResizeEvent, EventTypes_d_ObjectSelectedEvent as ObjectSelectedEvent, EventTypes_d_ScrollIntoViewEvent as ScrollIntoViewEvent, EventTypes_d_SetSelectionRangeEvent as SetSelectionRangeEvent, EventTypes_d_ShowCaretEvent as ShowCaretEvent, EventTypes_d_SwitchModeEvent as SwitchModeEvent, EventTypes_d_ChangeEvent as ChangeEvent, EventTypes_d_AddUndoEvent as AddUndoEvent, EventTypes_d_UndoRedoEvent as UndoRedoEvent, EventTypes_d_WindowEvent as WindowEvent, EventTypes_d_ProgressStateEvent as ProgressStateEvent, EventTypes_d_AfterProgressStateEvent as AfterProgressStateEvent, EventTypes_d_PlaceholderToggleEvent as PlaceholderToggleEvent, EventTypes_d_LoadErrorEvent as LoadErrorEvent, EventTypes_d_PreProcessEvent as PreProcessEvent, EventTypes_d_PostProcessEvent as PostProcessEvent, EventTypes_d_PastePlainTextToggleEvent as PastePlainTextToggleEvent, EventTypes_d_PastePreProcessEvent as PastePreProcessEvent, EventTypes_d_PastePostProcessEvent as PastePostProcessEvent, EventTypes_d_NewTableRowEvent as NewTableRowEvent, EventTypes_d_NewTableCellEvent as NewTableCellEvent, EventTypes_d_TableEventData as TableEventData, EventTypes_d_TableModifiedEvent as TableModifiedEvent, EventTypes_d_BeforeOpenNotificationEvent as BeforeOpenNotificationEvent, EventTypes_d_OpenNotificationEvent as OpenNotificationEvent, EventTypes_d_EditorEventMap as EditorEventMap, EventTypes_d_EditorManagerEventMap as EditorManagerEventMap, }; } type Format_d_Formats = Formats; type Format_d_Format = Format; type Format_d_ApplyFormat = ApplyFormat; type Format_d_BlockFormat = BlockFormat; type Format_d_InlineFormat = InlineFormat; type Format_d_SelectorFormat = SelectorFormat; type Format_d_RemoveFormat = RemoveFormat; type Format_d_RemoveBlockFormat = RemoveBlockFormat; type Format_d_RemoveInlineFormat = RemoveInlineFormat; type Format_d_RemoveSelectorFormat = RemoveSelectorFormat; declare namespace Format_d { export { Format_d_Formats as Formats, Format_d_Format as Format, Format_d_ApplyFormat as ApplyFormat, Format_d_BlockFormat as BlockFormat, Format_d_InlineFormat as InlineFormat, Format_d_SelectorFormat as SelectorFormat, Format_d_RemoveFormat as RemoveFormat, Format_d_RemoveBlockFormat as RemoveBlockFormat, Format_d_RemoveInlineFormat as RemoveInlineFormat, Format_d_RemoveSelectorFormat as RemoveSelectorFormat, }; } declare type StyleFormat = BlockStyleFormat | InlineStyleFormat | SelectorStyleFormat; declare type AllowedFormat = Separator | FormatReference | StyleFormat | NestedFormatting; interface Separator { title: string; } interface FormatReference { title: string; format: string; icon?: string; } interface NestedFormatting { title: string; items: Array; } interface CommonStyleFormat { name?: string; title: string; icon?: string; } interface BlockStyleFormat extends BlockFormat, CommonStyleFormat { } interface InlineStyleFormat extends InlineFormat, CommonStyleFormat { } interface SelectorStyleFormat extends SelectorFormat, CommonStyleFormat { } declare type EntityEncoding = 'named' | 'numeric' | 'raw' | 'named,numeric' | 'named+numeric' | 'numeric,named' | 'numeric+named'; interface ContentLanguage { readonly title: string; readonly code: string; readonly customCode?: string; } declare type ThemeInitFunc = (editor: Editor, elm: HTMLElement) => { editorContainer: HTMLElement; iframeContainer: HTMLElement; height?: number; iframeHeight?: number; api?: EditorUiApi; }; declare type SetupCallback = (editor: Editor) => void; declare type FilePickerCallback = (callback: (value: string, meta?: Record) => void, value: string, meta: Record) => void; declare type FilePickerValidationStatus = 'valid' | 'unknown' | 'invalid' | 'none'; declare type FilePickerValidationCallback = (info: { type: string; url: string; }, callback: (validation: { status: FilePickerValidationStatus; message: string; }) => void) => void; declare type PastePreProcessFn = (editor: Editor, args: PastePreProcessEvent) => void; declare type PastePostProcessFn = (editor: Editor, args: PastePostProcessEvent) => void; declare type URLConverter = (url: string, name: string, elm?: string | Element) => string; declare type URLConverterCallback = (url: string, node: Node | string | undefined, on_save: boolean, name: string) => string; interface ToolbarGroup { name?: string; items: string[]; } declare type ToolbarMode = 'floating' | 'sliding' | 'scrolling' | 'wrap'; declare type ToolbarLocation = 'top' | 'bottom' | 'auto'; interface BaseEditorOptions { a11y_advanced_options?: boolean; add_form_submit_trigger?: boolean; add_unload_trigger?: boolean; allow_conditional_comments?: boolean; allow_html_data_urls?: boolean; allow_html_in_named_anchor?: boolean; allow_script_urls?: boolean; allow_svg_data_urls?: boolean; allow_unsafe_link_target?: boolean; anchor_bottom?: false | string; anchor_top?: false | string; auto_focus?: string | true; automatic_uploads?: boolean; base_url?: string; block_formats?: string; block_unsupported_drop?: boolean; body_id?: string; body_class?: string; br_in_pre?: boolean; br_newline_selector?: string; browser_spellcheck?: boolean; branding?: boolean; cache_suffix?: string; color_cols?: number; color_cols_foreground?: number; color_cols_background?: number; color_map?: string[]; color_map_foreground?: string[]; color_map_background?: string[]; color_default_foreground?: string; color_default_background?: string; content_css?: boolean | string | string[]; content_css_cors?: boolean; content_security_policy?: string; content_style?: string; content_langs?: ContentLanguage[]; contextmenu?: string | string[] | false; contextmenu_never_use_native?: boolean; convert_fonts_to_spans?: boolean; convert_urls?: boolean; custom_colors?: boolean; custom_elements?: string; custom_ui_selector?: string; custom_undo_redo_levels?: number; deprecation_warnings?: boolean; directionality?: 'ltr' | 'rtl'; doctype?: string; document_base_url?: string; draggable_modal?: boolean; editable_class?: string; element_format?: 'xhtml' | 'html'; elementpath?: boolean; encoding?: string; end_container_on_empty_block?: boolean | string; entities?: string; entity_encoding?: EntityEncoding; extended_valid_elements?: string; event_root?: string; file_picker_callback?: FilePickerCallback; file_picker_types?: string; file_picker_validator_handler?: FilePickerValidationCallback; fix_list_elements?: boolean; fixed_toolbar_container?: string; fixed_toolbar_container_target?: HTMLElement; font_css?: string | string[]; font_family_formats?: string; font_size_classes?: string; font_size_legacy_values?: string; font_size_style_values?: string; font_size_formats?: string; forced_root_block?: string; forced_root_block_attrs?: Record; formats?: Formats; format_noneditable_selector?: string; height?: number | string; hidden_input?: boolean; icons?: string; icons_url?: string; id?: string; iframe_aria_text?: string; iframe_attrs?: Record; images_file_types?: string; images_replace_blob_uris?: boolean; images_reuse_filename?: boolean; images_upload_base_path?: string; images_upload_credentials?: boolean; images_upload_handler?: UploadHandler; images_upload_url?: string; indent?: boolean; indent_after?: string; indent_before?: string; indent_use_margin?: boolean; indentation?: string; init_instance_callback?: SetupCallback; inline?: boolean; inline_boundaries?: boolean; inline_boundaries_selector?: string; inline_styles?: boolean; invalid_elements?: string; invalid_styles?: string | Record; keep_styles?: boolean; language?: string; language_load?: boolean; language_url?: string; line_height_formats?: string; max_height?: number; max_width?: number; menu?: Record; menubar?: boolean | string; min_height?: number; min_width?: number; model?: string; model_url?: string; newline_behavior?: 'block' | 'linebreak' | 'invert' | 'default'; no_newline_selector?: string; noneditable_class?: string; noneditable_regexp?: RegExp | RegExp[]; nowrap?: boolean; object_resizing?: boolean | string; paste_as_text?: boolean; paste_block_drop?: boolean; paste_data_images?: boolean; paste_merge_formats?: boolean; paste_postprocess?: PastePostProcessFn; paste_preprocess?: PastePreProcessFn; paste_remove_styles_if_webkit?: boolean; paste_tab_spaces?: number; paste_webkit_styles?: string; placeholder?: string; preserve_cdata?: boolean; preview_styles?: false | string; promotion?: boolean; protect?: RegExp[]; readonly?: boolean; referrer_policy?: ReferrerPolicy; relative_urls?: boolean; remove_script_host?: boolean; remove_trailing_brs?: boolean; removed_menuitems?: string; resize?: boolean | 'both'; resize_img_proportional?: boolean; root_name?: string; schema?: SchemaType; selector?: string; setup?: SetupCallback; sidebar_show?: string; skin?: boolean | string; skin_url?: string; smart_paste?: boolean; statusbar?: boolean; style_formats?: AllowedFormat[]; style_formats_autohide?: boolean; style_formats_merge?: boolean; submit_patch?: boolean; suffix?: string; table_tab_navigation?: boolean; target?: HTMLElement; text_patterns?: RawPattern[] | false; text_patterns_lookup?: RawDynamicPatternsLookup; theme?: string | ThemeInitFunc | false; theme_url?: string; toolbar?: boolean | string | string[] | Array; toolbar1?: string; toolbar2?: string; toolbar3?: string; toolbar4?: string; toolbar5?: string; toolbar6?: string; toolbar7?: string; toolbar8?: string; toolbar9?: string; toolbar_groups?: Record; toolbar_location?: ToolbarLocation; toolbar_mode?: ToolbarMode; toolbar_sticky?: boolean; toolbar_sticky_offset?: number; typeahead_urls?: boolean; url_converter?: URLConverter; url_converter_scope?: any; urlconverter_callback?: URLConverterCallback; valid_children?: string; valid_classes?: string | Record; valid_elements?: string; valid_styles?: string | Record; verify_html?: boolean; visual?: boolean; visual_anchor_class?: string; visual_table_class?: string; width?: number | string; disable_nodechange?: boolean; forced_plugins?: string | string[]; plugin_base_urls?: Record; service_message?: string; [key: string]: any; } interface RawEditorOptions extends BaseEditorOptions { external_plugins?: Record; mobile?: RawEditorOptions; plugins?: string | string[]; } interface NormalizedEditorOptions extends BaseEditorOptions { external_plugins: Record; forced_plugins: string[]; plugins: string[]; } interface EditorOptions extends NormalizedEditorOptions { a11y_advanced_options: boolean; allow_unsafe_link_target: boolean; anchor_bottom: string; anchor_top: string; automatic_uploads: boolean; block_formats: string; body_class: string; body_id: string; br_newline_selector: string; color_map: string[]; color_cols: number; color_cols_foreground: number; color_cols_background: number; color_default_background: string; color_default_foreground: string; content_css: string[]; contextmenu: string[]; custom_colors: boolean; document_base_url: string; draggable_modal: boolean; editable_class: string; font_css: string[]; font_family_formats: string; font_size_classes: string; font_size_formats: string; font_size_legacy_values: string; font_size_style_values: string; forced_root_block: string; forced_root_block_attrs: Record; format_noneditable_selector: string; height: number | string; iframe_attrs: Record; images_file_types: string; images_upload_base_path: string; images_upload_credentials: boolean; images_upload_url: string; indent_use_margin: boolean; indentation: string; inline: boolean; inline_boundaries_selector: string; language: string; language_load: boolean; language_url: string; line_height_formats: string; menu: Record; menubar: boolean | string; model: string; no_newline_selector: string; noneditable_class: string; noneditable_regexp: RegExp[]; object_resizing: string; paste_as_text: boolean; preview_styles: string; promotion: boolean; readonly: boolean; removed_menuitems: string; toolbar: boolean | string | string[] | Array; toolbar_groups: Record; toolbar_location: ToolbarLocation; toolbar_mode: ToolbarMode; toolbar_persist: boolean; toolbar_sticky: boolean; toolbar_sticky_offset: number; text_patterns: Pattern[]; text_patterns_lookup: DynamicPatternsLookup; visual: boolean; visual_anchor_class: string; visual_table_class: string; width: number | string; } declare type StyleMap = Record; interface StylesSettings { allow_script_urls?: boolean; allow_svg_data_urls?: boolean; url_converter?: URLConverter; url_converter_scope?: any; } interface Styles { parse: (css: string | undefined) => Record; serialize: (styles: StyleMap, elementName?: string) => string; } declare type EventUtilsCallback = (event: EventUtilsEvent) => void | boolean; declare type EventUtilsEvent = NormalizedEvent & { metaKey: boolean; }; interface Callback$1 { func: EventUtilsCallback; scope: any; } interface CallbackList extends Array> { fakeName: string | false; capture: boolean; nativeHandler: EventListener; } interface EventUtilsConstructor { readonly prototype: EventUtils; new (): EventUtils; Event: EventUtils; } declare class EventUtils { static Event: EventUtils; domLoaded: boolean; events: Record>>; private readonly expando; private hasFocusIn; private count; constructor(); bind(target: any, name: K, callback: EventUtilsCallback, scope?: any): EventUtilsCallback; bind(target: any, names: string, callback: EventUtilsCallback, scope?: any): EventUtilsCallback; unbind(target: any, name: K, callback?: EventUtilsCallback): this; unbind(target: any, names: string, callback?: EventUtilsCallback): this; unbind(target: any): this; fire(target: any, name: string, args?: {}): this; dispatch(target: any, name: string, args?: {}): this; clean(target: any): this; destroy(): void; cancel(e: EventUtilsEvent): boolean; private executeHandlers; } interface SetAttribEvent { attrElm: HTMLElement; attrName: string; attrValue: string | boolean | number | null; } interface DOMUtilsSettings { schema: Schema; url_converter: URLConverter; url_converter_scope: any; ownEvents: boolean; keep_values: boolean; update_styles: boolean; root_element: HTMLElement | null; collect: boolean; onSetAttrib: (event: SetAttribEvent) => void; contentCssCors: boolean; referrerPolicy: ReferrerPolicy; } declare type Target = Node | Window; declare type RunArguments = string | T | Array | null; declare type BoundEvent = [ Target, string, EventUtilsCallback, any ]; declare type Callback = EventUtilsCallback>; declare type RunResult = T extends Array ? R[] : false | R; interface DOMUtils { doc: Document; settings: Partial; win: Window; files: Record; stdMode: boolean; boxModel: boolean; styleSheetLoader: StyleSheetLoader; boundEvents: BoundEvent[]; styles: Styles; schema: Schema; events: EventUtils; root: Node | null; isBlock: { (node: Node | null): node is HTMLElement; (node: string): boolean; }; clone: (node: Node, deep: boolean) => Node; getRoot: () => HTMLElement; getViewPort: (argWin?: Window) => GeomRect; getRect: (elm: string | HTMLElement) => GeomRect; getSize: (elm: string | HTMLElement) => { w: number; h: number; }; getParent: { (node: string | Node | null, selector: K, root?: Node): HTMLElementTagNameMap[K] | null; (node: string | Node | null, selector: string | ((node: Node) => node is T), root?: Node): T | null; (node: string | Node | null, selector?: string | ((node: Node) => boolean | void), root?: Node): Node | null; }; getParents: { (elm: string | HTMLElementTagNameMap[K] | null, selector: K, root?: Node, collect?: boolean): Array; (node: string | Node | null, selector: string | ((node: Node) => node is T), root?: Node, collect?: boolean): T[]; (elm: string | Node | null, selector?: string | ((node: Node) => boolean | void), root?: Node, collect?: boolean): Node[]; }; get: { (elm: T): T; (elm: string): HTMLElement | null; }; getNext: (node: Node | null, selector: string | ((node: Node) => boolean)) => Node | null; getPrev: (node: Node | null, selector: string | ((node: Node) => boolean)) => Node | null; select: { (selector: K, scope?: string | Node): Array; (selector: string, scope?: string | Node): T[]; }; is: { (elm: Node | Node[] | null, selector: string): elm is T; (elm: Node | Node[] | null, selector: string): boolean; }; add: (parentElm: RunArguments, name: string | Element, attrs?: Record, html?: string | Node | null, create?: boolean) => HTMLElement; create: { (name: K, attrs?: Record, html?: string | Node | null): HTMLElementTagNameMap[K]; (name: string, attrs?: Record, html?: string | Node | null): HTMLElement; }; createHTML: (name: string, attrs?: Record, html?: string) => string; createFragment: (html?: string) => DocumentFragment; remove: { (node: T | T[], keepChildren?: boolean): typeof node extends Array ? T[] : T; (node: string, keepChildren?: boolean): T | false; }; getStyle: { (elm: Element, name: string, computed: true): string; (elm: string | Element | null, name: string, computed?: boolean): string | undefined; }; setStyle: (elm: string | Element | Element[], name: string, value: string | number | null) => void; setStyles: (elm: string | Element | Element[], stylesArg: StyleMap) => void; removeAllAttribs: (e: RunArguments) => void; setAttrib: (elm: RunArguments, name: string, value: string | boolean | number | null) => void; setAttribs: (elm: RunArguments, attrs: Record) => void; getAttrib: (elm: string | Element | null, name: string, defaultVal?: string) => string; getAttribs: (elm: string | Element) => NamedNodeMap | Attr[]; getPos: (elm: string | Element, rootElm?: Node) => { x: number; y: number; }; parseStyle: (cssText: string) => Record; serializeStyle: (stylesArg: StyleMap, name?: string) => string; addStyle: (cssText: string) => void; loadCSS: (url: string) => void; hasClass: (elm: string | Element, cls: string) => boolean; addClass: (elm: RunArguments, cls: string) => void; removeClass: (elm: RunArguments, cls: string) => void; toggleClass: (elm: RunArguments, cls: string, state?: boolean) => void; show: (elm: string | Node | Node[]) => void; hide: (elm: string | Node | Node[]) => void; isHidden: (elm: string | Node) => boolean; uniqueId: (prefix?: string) => string; setHTML: (elm: RunArguments, html: string) => void; getOuterHTML: (elm: string | Node) => string; setOuterHTML: (elm: string | Node | Node[], html: string) => void; decode: (text: string) => string; encode: (text: string) => string; insertAfter: { (node: T | T[], reference: string | Node): T; (node: RunArguments, reference: string | Node): RunResult; }; replace: { (newElm: Node, oldElm: T | T[], keepChildren?: boolean): T; (newElm: Node, oldElm: RunArguments, keepChildren?: boolean): false | T; }; rename: { (elm: Element, name: K): HTMLElementTagNameMap[K]; (elm: Element, name: string): Element; }; findCommonAncestor: (a: Node, b: Node) => Node | null; run(this: DOMUtils, elm: T | T[], func: (node: T) => R, scope?: any): typeof elm extends Array ? R[] : R; run(this: DOMUtils, elm: RunArguments, func: (node: T) => R, scope?: any): RunResult; isEmpty: (node: Node, elements?: Record) => boolean; createRng: () => Range; nodeIndex: (node: Node, normalized?: boolean) => number; split: { (parentElm: Node, splitElm: Node, replacementElm: T): T | undefined; (parentElm: Node, splitElm: T): T | undefined; }; bind: { (target: Target, name: K, func: Callback, scope?: any): Callback; (target: Target[], name: K, func: Callback, scope?: any): Callback[]; }; unbind: { (target: Target, name?: K, func?: EventUtilsCallback>): EventUtils; (target: Target[], name?: K, func?: EventUtilsCallback>): EventUtils[]; }; fire: (target: Node | Window, name: string, evt?: {}) => EventUtils; dispatch: (target: Node | Window, name: string, evt?: {}) => EventUtils; getContentEditable: (node: Node) => string | null; getContentEditableParent: (node: Node) => string | null; destroy: () => void; isChildOf: (node: Node, parent: Node) => boolean; dumpRng: (r: Range) => string; } interface ClientRect { left: number; top: number; bottom: number; right: number; width: number; height: number; } interface BookmarkManager { getBookmark: (type?: number, normalized?: boolean) => Bookmark; moveToBookmark: (bookmark: Bookmark) => void; } interface ControlSelection { isResizable: (elm: Element) => boolean; showResizeRect: (elm: HTMLElement) => void; hideResizeRect: () => void; updateResizeRect: (evt: EditorEvent) => void; destroy: () => void; } interface WriterSettings { element_format?: 'xhtml' | 'html'; entities?: string; entity_encoding?: EntityEncoding; indent?: boolean; indent_after?: string; indent_before?: string; } declare type Attributes = Array<{ name: string; value: string; }>; interface Writer { cdata: (text: string) => void; comment: (text: string) => void; doctype: (text: string) => void; end: (name: string) => void; getContent: () => string; pi: (name: string, text?: string) => void; reset: () => void; start: (name: string, attrs?: Attributes | null, empty?: boolean) => void; text: (text: string, raw?: boolean) => void; } interface HtmlSerializerSettings extends WriterSettings { inner?: boolean; validate?: boolean; } interface HtmlSerializer { serialize: (node: AstNode) => string; } interface DomSerializerSettings extends DomParserSettings, WriterSettings, SchemaSettings, HtmlSerializerSettings { url_converter?: URLConverter; url_converter_scope?: {}; } interface DomSerializerImpl { schema: Schema; addNodeFilter: (name: string, callback: ParserFilterCallback) => void; addAttributeFilter: (name: string, callback: ParserFilterCallback) => void; getNodeFilters: () => ParserFilter[]; getAttributeFilters: () => ParserFilter[]; removeNodeFilter: (name: string, callback?: ParserFilterCallback) => void; removeAttributeFilter: (name: string, callback?: ParserFilterCallback) => void; serialize: { (node: Element, parserArgs: { format: 'tree'; } & ParserArgs): AstNode; (node: Element, parserArgs?: ParserArgs): string; }; addRules: (rules: string) => void; setRules: (rules: string) => void; addTempAttr: (name: string) => void; getTempAttrs: () => string[]; } interface DomSerializer extends DomSerializerImpl { } interface EditorSelection { bookmarkManager: BookmarkManager; controlSelection: ControlSelection; dom: DOMUtils; win: Window; serializer: DomSerializer; editor: Editor; collapse: (toStart?: boolean) => void; setCursorLocation: { (node: Node, offset: number): void; (): void; }; getContent: { (args: { format: 'tree'; } & Partial): AstNode; (args?: Partial): string; }; setContent: (content: string, args?: Partial) => void; getBookmark: (type?: number, normalized?: boolean) => Bookmark; moveToBookmark: (bookmark: Bookmark) => void; select: (node: Node, content?: boolean) => Node; isCollapsed: () => boolean; isForward: () => boolean; setNode: (elm: Element) => Element; getNode: () => HTMLElement; getSel: () => Selection | null; setRng: (rng: Range, forward?: boolean) => void; getRng: () => Range; getStart: (real?: boolean) => Element; getEnd: (real?: boolean) => Element; getSelectedBlocks: (startElm?: Element, endElm?: Element) => Element[]; normalize: () => Range; selectorChanged: (selector: string, callback: (active: boolean, args: { node: Node; selector: String; parents: Node[]; }) => void) => EditorSelection; selectorChangedWithUnbind: (selector: string, callback: (active: boolean, args: { node: Node; selector: String; parents: Node[]; }) => void) => { unbind: () => void; }; getScrollContainer: () => HTMLElement | undefined; scrollIntoView: (elm?: HTMLElement, alignToTop?: boolean) => void; placeCaretAt: (clientX: number, clientY: number) => void; getBoundingClientRect: () => ClientRect | DOMRect; destroy: () => void; expand: (options?: { type: 'word'; }) => void; } declare type EditorCommandCallback = (this: S, ui: boolean, value: any) => void; declare type EditorCommandsCallback = (command: string, ui: boolean, value?: any) => void; interface Commands { state: Record boolean>; exec: Record; value: Record string>; } interface ExecCommandArgs { skip_focus?: boolean; } interface EditorCommandsConstructor { readonly prototype: EditorCommands; new (editor: Editor): EditorCommands; } declare class EditorCommands { private readonly editor; private commands; constructor(editor: Editor); execCommand(command: string, ui?: boolean, value?: any, args?: ExecCommandArgs): boolean; queryCommandState(command: string): boolean; queryCommandValue(command: string): string; addCommands(commandList: Commands[K], type: K): void; addCommands(commandList: Record): void; addCommand(command: string, callback: EditorCommandCallback, scope: S): void; addCommand(command: string, callback: EditorCommandCallback): void; queryCommandSupported(command: string): boolean; addQueryStateHandler(command: string, callback: (this: S) => boolean, scope: S): void; addQueryStateHandler(command: string, callback: (this: Editor) => boolean): void; addQueryValueHandler(command: string, callback: (this: S) => string, scope: S): void; addQueryValueHandler(command: string, callback: (this: Editor) => string): void; } interface RawString { raw: string; } declare type Primitive = string | number | boolean | Record | Function; declare type TokenisedString = [ string, ...Primitive[] ]; declare type Untranslated = Primitive | TokenisedString | RawString | null | undefined; declare type TranslatedString = string; interface I18n { getData: () => Record>; setCode: (newCode: string) => void; getCode: () => string; add: (code: string, items: Record) => void; translate: (text: Untranslated) => TranslatedString; isRtl: () => boolean; hasCode: (code: string) => boolean; } interface Observable { fire>(name: K, args?: U, bubble?: boolean): EditorEvent; dispatch>(name: K, args?: U, bubble?: boolean): EditorEvent; on(name: K, callback: (event: EditorEvent>) => void, prepend?: boolean): EventDispatcher; off(name?: K, callback?: (event: EditorEvent>) => void): EventDispatcher; once(name: K, callback: (event: EditorEvent>) => void): EventDispatcher; hasEventListeners(name: string): boolean; } interface URISettings { base_uri?: URI; } interface URIConstructor { readonly prototype: URI; new (url: string, settings?: URISettings): URI; getDocumentBaseUrl: (loc: { protocol: string; host?: string; href?: string; pathname?: string; }) => string; parseDataUri: (uri: string) => { type: string; data: string; }; } interface SafeUriOptions { readonly allow_html_data_urls?: boolean; readonly allow_script_urls?: boolean; readonly allow_svg_data_urls?: boolean; } declare class URI { static parseDataUri(uri: string): { type: string | undefined; data: string; }; static isDomSafe(uri: string, context?: string, options?: SafeUriOptions): boolean; static getDocumentBaseUrl(loc: { protocol: string; host?: string; href?: string; pathname?: string; }): string; source: string; protocol: string | undefined; authority: string | undefined; userInfo: string | undefined; user: string | undefined; password: string | undefined; host: string | undefined; port: string | undefined; relative: string | undefined; path: string; directory: string; file: string | undefined; query: string | undefined; anchor: string | undefined; settings: URISettings; constructor(url: string, settings?: URISettings); setPath(path: string): void; toRelative(uri: string): string; toAbsolute(uri: string, noHost?: boolean): string; isSameOrigin(uri: URI): boolean; toRelPath(base: string, path: string): string; toAbsPath(base: string, path: string): string; getURI(noProtoHost?: boolean): string; } interface EditorManager extends Observable { defaultOptions: RawEditorOptions; majorVersion: string; minorVersion: string; releaseDate: string; activeEditor: Editor | null; focusedEditor: Editor | null; baseURI: URI; baseURL: string; documentBaseURL: string; i18n: I18n; suffix: string; add(this: EditorManager, editor: Editor): Editor; addI18n: (code: string, item: Record) => void; createEditor(this: EditorManager, id: string, options: RawEditorOptions): Editor; execCommand(this: EditorManager, cmd: string, ui: boolean, value: any): boolean; get(this: EditorManager): Editor[]; get(this: EditorManager, id: number | string): Editor | null; init(this: EditorManager, options: RawEditorOptions): Promise; overrideDefaults(this: EditorManager, defaultOptions: Partial): void; remove(this: EditorManager): void; remove(this: EditorManager, selector: string): void; remove(this: EditorManager, editor: Editor): Editor | null; setActive(this: EditorManager, editor: Editor): void; setup(this: EditorManager): void; translate: (text: Untranslated) => TranslatedString; triggerSave: () => void; _setBaseUrl(this: EditorManager, baseUrl: string): void; } interface EditorObservable extends Observable { bindPendingEventDelegates(this: Editor): void; toggleNativeEvent(this: Editor, name: string, state: boolean): void; unbindAllNativeEvents(this: Editor): void; } interface ProcessorSuccess { valid: true; value: T; } interface ProcessorError { valid: false; message: string; } declare type SimpleProcessor = (value: unknown) => boolean; declare type Processor = (value: unknown) => ProcessorSuccess | ProcessorError; interface BuiltInOptionTypeMap { 'string': string; 'number': number; 'boolean': boolean; 'array': any[]; 'function': Function; 'object': any; 'string[]': string[]; 'object[]': any[]; 'regexp': RegExp; } declare type BuiltInOptionType = keyof BuiltInOptionTypeMap; interface BaseOptionSpec { immutable?: boolean; deprecated?: boolean; docsUrl?: string; } interface BuiltInOptionSpec extends BaseOptionSpec { processor: K; default?: BuiltInOptionTypeMap[K]; } interface SimpleOptionSpec extends BaseOptionSpec { processor: SimpleProcessor; default?: T; } interface OptionSpec extends BaseOptionSpec { processor: Processor; default?: T; } interface Options { register: { (name: string, spec: BuiltInOptionSpec): void; (name: K, spec: OptionSpec | SimpleOptionSpec): void; (name: string, spec: OptionSpec): void; (name: string, spec: SimpleOptionSpec): void; }; isRegistered: (name: string) => boolean; get: { (name: K): EditorOptions[K]; (name: string): T | undefined; }; set: (name: K, value: K extends keyof NormalizedEditorOptions ? NormalizedEditorOptions[K] : T) => boolean; unset: (name: string) => boolean; isSet: (name: string) => boolean; } interface UploadResult$1 { element: HTMLImageElement; status: boolean; blobInfo: BlobInfo; uploadUri: string; removed: boolean; } interface EditorUpload { blobCache: BlobCache; addFilter: (filter: (img: HTMLImageElement) => boolean) => void; uploadImages: () => Promise; uploadImagesAuto: () => Promise; scanForImages: () => Promise; destroy: () => void; } declare type FormatChangeCallback = (state: boolean, data: { node: Node; format: string; parents: Element[]; }) => void; interface FormatRegistry { get: { (name: string): Format[] | undefined; (): Record; }; has: (name: string) => boolean; register: (name: string | Formats, format?: Format[] | Format) => void; unregister: (name: string) => Formats; } interface Formatter extends FormatRegistry { apply: (name: string, vars?: FormatVars, node?: Node | RangeLikeObject | null) => void; remove: (name: string, vars?: FormatVars, node?: Node | Range, similar?: boolean) => void; toggle: (name: string, vars?: FormatVars, node?: Node) => void; match: (name: string, vars?: FormatVars, node?: Node, similar?: boolean) => boolean; closest: (names: string[]) => string | null; matchAll: (names: string[], vars?: FormatVars) => string[]; matchNode: (node: Node | null, name: string, vars?: FormatVars, similar?: boolean) => Format | undefined; canApply: (name: string) => boolean; formatChanged: (names: string, callback: FormatChangeCallback, similar?: boolean, vars?: FormatVars) => { unbind: () => void; }; getCssText: (format: string | ApplyFormat) => string; } interface EditorMode { isReadOnly: () => boolean; set: (mode: string) => void; get: () => string; register: (mode: string, api: EditorModeApi) => void; } interface EditorModeApi { activate: () => void; deactivate: () => void; editorReadOnly: boolean; } interface Model { readonly table: { readonly getSelectedCells: () => HTMLTableCellElement[]; readonly clearSelectedCells: (container: Node) => void; }; } declare type ModelManager = AddOnManager; interface Plugin { getMetadata?: () => { name: string; url: string; }; init?: (editor: Editor, url: string) => void; [key: string]: any; } declare type PluginManager = AddOnManager; interface ShortcutsConstructor { readonly prototype: Shortcuts; new (editor: Editor): Shortcuts; } declare type CommandFunc = string | [ string, boolean, any ] | (() => void); declare class Shortcuts { private readonly editor; private readonly shortcuts; private pendingPatterns; constructor(editor: Editor); add(pattern: string, desc: string | null, cmdFunc: CommandFunc, scope?: any): boolean; remove(pattern: string): boolean; private normalizeCommandFunc; private createShortcut; private hasModifier; private isFunctionKey; private matchShortcut; private executeShortcutAction; } interface RenderResult { iframeContainer?: HTMLElement; editorContainer: HTMLElement; api?: Partial; } interface Theme { ui?: any; inline?: any; execCommand?: (command: string, ui?: boolean, value?: any) => boolean; destroy?: () => void; init?: (editor: Editor, url: string) => void; renderUI?: () => RenderResult; getNotificationManagerImpl?: () => NotificationManagerImpl; getWindowManagerImpl?: () => WindowManagerImpl; } declare type ThemeManager = AddOnManager; interface EditorConstructor { readonly prototype: Editor; new (id: string, options: RawEditorOptions, editorManager: EditorManager): Editor; } declare class Editor implements EditorObservable { documentBaseUrl: string; baseUri: URI; id: string; plugins: Record; documentBaseURI: URI; baseURI: URI; contentCSS: string[]; contentStyles: string[]; ui: EditorUi; mode: EditorMode; options: Options; shortcuts: Shortcuts; loadedCSS: Record; editorCommands: EditorCommands; suffix: string; editorManager: EditorManager; hidden: boolean; inline: boolean; hasVisual: boolean; isNotDirty: boolean; annotator: Annotator; bodyElement: HTMLElement | undefined; bookmark: any; composing: boolean; container: HTMLElement; contentAreaContainer: HTMLElement; contentDocument: Document; contentWindow: Window; delegates: Record> | undefined; destroyed: boolean; dom: DOMUtils; editorContainer: HTMLElement; editorUpload: EditorUpload; eventRoot: Element | undefined; formatter: Formatter; formElement: HTMLElement | undefined; formEventDelegate: ((e: Event) => void) | undefined; hasHiddenInput: boolean; iframeElement: HTMLIFrameElement | null; iframeHTML: string | undefined; initialized: boolean; notificationManager: NotificationManager; orgDisplay: string; orgVisibility: string | undefined; parser: DomParser; quirks: Quirks; readonly: boolean; removed: boolean; schema: Schema; selection: EditorSelection; serializer: DomSerializer; startContent: string; targetElm: HTMLElement; theme: Theme; model: Model; undoManager: UndoManager; windowManager: WindowManager; _beforeUnload: (() => void) | undefined; _eventDispatcher: EventDispatcher | undefined; _nodeChangeDispatcher: NodeChange; _pendingNativeEvents: string[]; _selectionOverrides: SelectionOverrides; _skinLoaded: boolean; bindPendingEventDelegates: EditorObservable['bindPendingEventDelegates']; toggleNativeEvent: EditorObservable['toggleNativeEvent']; unbindAllNativeEvents: EditorObservable['unbindAllNativeEvents']; fire: EditorObservable['fire']; dispatch: EditorObservable['dispatch']; on: EditorObservable['on']; off: EditorObservable['off']; once: EditorObservable['once']; hasEventListeners: EditorObservable['hasEventListeners']; constructor(id: string, options: RawEditorOptions, editorManager: EditorManager); render(): void; focus(skipFocus?: boolean): void; hasFocus(): boolean; translate(text: Untranslated): TranslatedString; getParam(name: string, defaultVal: BuiltInOptionTypeMap[K], type: K): BuiltInOptionTypeMap[K]; getParam(name: K, defaultVal?: NormalizedEditorOptions[K], type?: BuiltInOptionType): NormalizedEditorOptions[K]; getParam(name: string, defaultVal: T, type?: BuiltInOptionType): T; hasPlugin(name: string, loaded?: boolean): boolean; nodeChanged(args?: any): void; addCommand(name: string, callback: EditorCommandCallback, scope: S): void; addCommand(name: string, callback: EditorCommandCallback): void; addQueryStateHandler(name: string, callback: (this: S) => boolean, scope?: S): void; addQueryStateHandler(name: string, callback: (this: Editor) => boolean): void; addQueryValueHandler(name: string, callback: (this: S) => string, scope: S): void; addQueryValueHandler(name: string, callback: (this: Editor) => string): void; addShortcut(pattern: string, desc: string, cmdFunc: string | [ string, boolean, any ] | (() => void), scope?: any): void; execCommand(cmd: string, ui?: boolean, value?: any, args?: ExecCommandArgs): boolean; queryCommandState(cmd: string): boolean; queryCommandValue(cmd: string): string; queryCommandSupported(cmd: string): boolean; show(): void; hide(): void; isHidden(): boolean; setProgressState(state: boolean, time?: number): void; load(args?: Partial): string; save(args?: Partial): string; setContent(content: string, args?: Partial): string; setContent(content: AstNode, args?: Partial): AstNode; setContent(content: Content, args?: Partial): Content; getContent(args: { format: 'tree'; } & Partial): AstNode; getContent(args?: Partial): string; insertContent(content: string, args?: any): void; resetContent(initialContent?: string): void; isDirty(): boolean; setDirty(state: boolean): void; getContainer(): HTMLElement; getContentAreaContainer(): HTMLElement; getElement(): HTMLElement; getWin(): Window; getDoc(): Document; getBody(): HTMLElement; convertURL(url: string, name: string, elm?: string | Element): string; addVisual(elm?: HTMLElement): void; remove(): void; destroy(automatic?: boolean): void; uploadImages(): Promise; _scanForImages(): Promise; } interface UrlObject { prefix: string; resource: string; suffix: string; } declare type WaitState = 'added' | 'loaded'; declare type AddOnConstructor = (editor: Editor, url: string) => T; interface AddOnManager { items: AddOnConstructor[]; urls: Record; lookup: Record; }>; get: (name: string) => AddOnConstructor | undefined; requireLangPack: (name: string, languages?: string) => void; add: (id: string, addOn: AddOnConstructor) => AddOnConstructor; remove: (name: string) => void; createUrl: (baseUrl: UrlObject, dep: string | UrlObject) => UrlObject; load: (name: string, addOnUrl: string | UrlObject) => Promise; waitFor: (name: string, state?: WaitState) => Promise; } interface RangeUtils { walk: (rng: Range, callback: (nodes: Node[]) => void) => void; split: (rng: Range) => RangeLikeObject; normalize: (rng: Range) => boolean; expand: (rng: Range, options?: { type: 'word'; }) => Range; } interface ScriptLoaderSettings { referrerPolicy?: ReferrerPolicy; } interface ScriptLoaderConstructor { readonly prototype: ScriptLoader; new (): ScriptLoader; ScriptLoader: ScriptLoader; } declare class ScriptLoader { static ScriptLoader: ScriptLoader; private settings; private states; private queue; private scriptLoadedCallbacks; private queueLoadedCallbacks; private loading; constructor(settings?: ScriptLoaderSettings); _setReferrerPolicy(referrerPolicy: ReferrerPolicy): void; loadScript(url: string): Promise; isDone(url: string): boolean; markDone(url: string): void; add(url: string): Promise; load(url: string): Promise; remove(url: string): void; loadQueue(): Promise; loadScripts(scripts: string[]): Promise; } declare type TextProcessCallback = (node: Text, offset: number, text: string) => number; interface Spot { container: Text; offset: number; } interface TextSeeker { backwards: (node: Node, offset: number, process: TextProcessCallback, root?: Node) => Spot | null; forwards: (node: Node, offset: number, process: TextProcessCallback, root?: Node) => Spot | null; } interface DomTreeWalkerConstructor { readonly prototype: DomTreeWalker; new (startNode: Node, rootNode: Node): DomTreeWalker; } declare class DomTreeWalker { private readonly rootNode; private node; constructor(startNode: Node, rootNode: Node); current(): Node | null | undefined; next(shallow?: boolean): Node | null | undefined; prev(shallow?: boolean): Node | null | undefined; prev2(shallow?: boolean): Node | null | undefined; private findSibling; private findPreviousNode; } interface Version { major: number; minor: number; } interface Env { transparentSrc: string; documentMode: number; cacheSuffix: any; container: any; canHaveCSP: boolean; windowsPhone: boolean; browser: { current: string | undefined; version: Version; isEdge: () => boolean; isChromium: () => boolean; isIE: () => boolean; isOpera: () => boolean; isFirefox: () => boolean; isSafari: () => boolean; }; os: { current: string | undefined; version: Version; isWindows: () => boolean; isiOS: () => boolean; isAndroid: () => boolean; isMacOS: () => boolean; isLinux: () => boolean; isSolaris: () => boolean; isFreeBSD: () => boolean; isChromeOS: () => boolean; }; deviceType: { isiPad: () => boolean; isiPhone: () => boolean; isTablet: () => boolean; isPhone: () => boolean; isTouch: () => boolean; isWebView: () => boolean; isDesktop: () => boolean; }; } interface FakeClipboardItem { readonly items: Record; readonly types: ReadonlyArray; readonly getType: (type: string) => D | undefined; } interface FakeClipboard { readonly FakeClipboardItem: (items: Record) => FakeClipboardItem; readonly write: (data: FakeClipboardItem[]) => void; readonly read: () => FakeClipboardItem[] | undefined; readonly clear: () => void; } interface FocusManager { isEditorUIElement: (elm: Element) => boolean; } interface EntitiesMap { [name: string]: string; } interface Entities { encodeRaw: (text: string, attr?: boolean) => string; encodeAllRaw: (text: string) => string; encodeNumeric: (text: string, attr?: boolean) => string; encodeNamed: (text: string, attr?: boolean, entities?: EntitiesMap) => string; getEncodeFunc: (name: string, entities?: string) => (text: string, attr?: boolean) => string; decode: (text: string) => string; } interface IconPack { icons: Record; } interface IconManager { add: (id: string, iconPack: IconPack) => void; get: (id: string) => IconPack; has: (id: string) => boolean; } interface Resource { load: (id: string, url: string) => Promise; add: (id: string, data: any) => void; unload: (id: string) => void; } type TextPatterns_d_Pattern = Pattern; type TextPatterns_d_RawPattern = RawPattern; type TextPatterns_d_DynamicPatternsLookup = DynamicPatternsLookup; type TextPatterns_d_RawDynamicPatternsLookup = RawDynamicPatternsLookup; type TextPatterns_d_DynamicPatternContext = DynamicPatternContext; type TextPatterns_d_BlockCmdPattern = BlockCmdPattern; type TextPatterns_d_BlockPattern = BlockPattern; type TextPatterns_d_BlockFormatPattern = BlockFormatPattern; type TextPatterns_d_InlineCmdPattern = InlineCmdPattern; type TextPatterns_d_InlinePattern = InlinePattern; type TextPatterns_d_InlineFormatPattern = InlineFormatPattern; declare namespace TextPatterns_d { export { TextPatterns_d_Pattern as Pattern, TextPatterns_d_RawPattern as RawPattern, TextPatterns_d_DynamicPatternsLookup as DynamicPatternsLookup, TextPatterns_d_RawDynamicPatternsLookup as RawDynamicPatternsLookup, TextPatterns_d_DynamicPatternContext as DynamicPatternContext, TextPatterns_d_BlockCmdPattern as BlockCmdPattern, TextPatterns_d_BlockPattern as BlockPattern, TextPatterns_d_BlockFormatPattern as BlockFormatPattern, TextPatterns_d_InlineCmdPattern as InlineCmdPattern, TextPatterns_d_InlinePattern as InlinePattern, TextPatterns_d_InlineFormatPattern as InlineFormatPattern, }; } interface Delay { setEditorInterval: (editor: Editor, callback: () => void, time?: number) => number; setEditorTimeout: (editor: Editor, callback: () => void, time?: number) => number; } declare type UploadResult = UploadResult$2; interface ImageUploader { upload: (blobInfos: BlobInfo[], showNotification?: boolean) => Promise; } declare type ArrayCallback$1 = (this: any, x: T, i: number, xs: ArrayLike) => R; declare type ObjCallback$1 = (this: any, value: T, key: string, obj: Record) => R; declare type ArrayCallback = ArrayCallback$1; declare type ObjCallback = ObjCallback$1; declare type WalkCallback = (this: any, o: T, i: string, n: keyof T | undefined) => boolean | void; interface Tools { is: (obj: any, type?: string) => boolean; isArray: (arr: any) => arr is Array; inArray: (arr: ArrayLike, value: T) => number; grep: { (arr: ArrayLike | null | undefined, pred?: ArrayCallback): T[]; (arr: Record | null | undefined, pred?: ObjCallback): T[]; }; trim: (str: string | null | undefined) => string; toArray: (obj: ArrayLike) => T[]; hasOwn: (obj: any, name: string) => boolean; makeMap: (items: ArrayLike | string | undefined, delim?: string | RegExp, map?: Record) => Record; each: { (arr: ArrayLike | null | undefined, cb: ArrayCallback, scope?: any): boolean; (obj: Record | null | undefined, cb: ObjCallback, scope?: any): boolean; }; map: { (arr: ArrayLike | null | undefined, cb: ArrayCallback): R[]; (obj: Record | null | undefined, cb: ObjCallback): R[]; }; extend: (obj: Object, ext: Object, ...objs: Object[]) => any; walk: >(obj: T, f: WalkCallback, n?: keyof T, scope?: any) => void; resolve: (path: string, o?: Object) => any; explode: (s: string | string[], d?: string | RegExp) => string[]; _addCacheSuffix: (url: string) => string; } interface KeyboardLikeEvent { shiftKey: boolean; ctrlKey: boolean; altKey: boolean; metaKey: boolean; } interface VK { BACKSPACE: number; DELETE: number; DOWN: number; ENTER: number; ESC: number; LEFT: number; RIGHT: number; SPACEBAR: number; TAB: number; UP: number; PAGE_UP: number; PAGE_DOWN: number; END: number; HOME: number; modifierPressed: (e: KeyboardLikeEvent) => boolean; metaKeyPressed: (e: KeyboardLikeEvent) => boolean; } interface DOMUtilsNamespace { (doc: Document, settings: Partial): DOMUtils; DOM: DOMUtils; nodeIndex: (node: Node, normalized?: boolean) => number; } interface RangeUtilsNamespace { (dom: DOMUtils): RangeUtils; compareRanges: (rng1: RangeLikeObject, rng2: RangeLikeObject) => boolean; getCaretRangeFromPoint: (clientX: number, clientY: number, doc: Document) => Range; getSelectedNode: (range: Range) => Node; getNode: (container: Node, offset: number) => Node; } interface AddOnManagerNamespace { (): AddOnManager; language: string | undefined; languageLoad: boolean; baseURL: string; PluginManager: PluginManager; ThemeManager: ThemeManager; ModelManager: ModelManager; } interface BookmarkManagerNamespace { (selection: EditorSelection): BookmarkManager; isBookmarkNode: (node: Node) => boolean; } interface TinyMCE extends EditorManager { geom: { Rect: Rect; }; util: { Delay: Delay; Tools: Tools; VK: VK; URI: URIConstructor; EventDispatcher: EventDispatcherConstructor; Observable: Observable; I18n: I18n; LocalStorage: Storage; ImageUploader: ImageUploader; }; dom: { EventUtils: EventUtilsConstructor; TreeWalker: DomTreeWalkerConstructor; TextSeeker: (dom: DOMUtils, isBlockBoundary?: (node: Node) => boolean) => TextSeeker; DOMUtils: DOMUtilsNamespace; ScriptLoader: ScriptLoaderConstructor; RangeUtils: RangeUtilsNamespace; Serializer: (settings: DomSerializerSettings, editor?: Editor) => DomSerializer; ControlSelection: (selection: EditorSelection, editor: Editor) => ControlSelection; BookmarkManager: BookmarkManagerNamespace; Selection: (dom: DOMUtils, win: Window, serializer: DomSerializer, editor: Editor) => EditorSelection; StyleSheetLoader: (documentOrShadowRoot: Document | ShadowRoot, settings: StyleSheetLoaderSettings) => StyleSheetLoader; Event: EventUtils; }; html: { Styles: (settings?: StylesSettings, schema?: Schema) => Styles; Entities: Entities; Node: AstNodeConstructor; Schema: (settings?: SchemaSettings) => Schema; DomParser: (settings?: DomParserSettings, schema?: Schema) => DomParser; Writer: (settings?: WriterSettings) => Writer; Serializer: (settings?: HtmlSerializerSettings, schema?: Schema) => HtmlSerializer; }; AddOnManager: AddOnManagerNamespace; Annotator: (editor: Editor) => Annotator; Editor: EditorConstructor; EditorCommands: EditorCommandsConstructor; EditorManager: EditorManager; EditorObservable: EditorObservable; Env: Env; FocusManager: FocusManager; Formatter: (editor: Editor) => Formatter; NotificationManager: (editor: Editor) => NotificationManager; Shortcuts: ShortcutsConstructor; UndoManager: (editor: Editor) => UndoManager; WindowManager: (editor: Editor) => WindowManager; DOM: DOMUtils; ScriptLoader: ScriptLoader; PluginManager: PluginManager; ThemeManager: ThemeManager; ModelManager: ModelManager; IconManager: IconManager; Resource: Resource; FakeClipboard: FakeClipboard; trim: Tools['trim']; isArray: Tools['isArray']; is: Tools['is']; toArray: Tools['toArray']; makeMap: Tools['makeMap']; each: Tools['each']; map: Tools['map']; grep: Tools['grep']; inArray: Tools['inArray']; extend: Tools['extend']; walk: Tools['walk']; resolve: Tools['resolve']; explode: Tools['explode']; _addCacheSuffix: Tools['_addCacheSuffix']; } declare const tinymce: TinyMCE; export { AddOnManager, Annotator, AstNode, Bookmark, BookmarkManager, ControlSelection, DOMUtils, Delay, DomParser, DomParserSettings, DomSerializer, DomSerializerSettings, DomTreeWalker, Editor, EditorCommands, EditorEvent, EditorManager, EditorModeApi, EditorObservable, EditorOptions, EditorSelection, Entities, Env, EventDispatcher, EventUtils, EventTypes_d as Events, FakeClipboard, FocusManager, Format_d as Formats, Formatter, GeomRect, HtmlSerializer, HtmlSerializerSettings, I18n, IconManager, Model, ModelManager, NotificationApi, NotificationManager, NotificationSpec, Observable, Plugin, PluginManager, RangeUtils, RawEditorOptions, Rect, Resource, Schema, SchemaSettings, ScriptLoader, Shortcuts, StyleSheetLoader, Styles, TextPatterns_d as TextPatterns, TextSeeker, Theme, ThemeManager, TinyMCE, Tools, URI, Ui_d as Ui, UndoManager, VK, WindowManager, Writer, WriterSettings, tinymce as default }; ================================================ FILE: vue_acimage_web/src/App.vue ================================================ ================================================ FILE: vue_acimage_web/src/api/HomeCarousel.js ================================================ import request from '@/utils/request.js' export let queryHomeCarousel = function() { return request.get('/api/community/homeCarousels/list'); } ================================================ FILE: vue_acimage_web/src/api/TopicSearch.js ================================================ import request from '@/utils/request.js' export let searchTopics = function(formData) { let config = { params: formData } return request.get('/api/community/topics/search/multiSearch', config) } ================================================ FILE: vue_acimage_web/src/api/UserRank.js ================================================ import request from '@/utils/request.js' import { Code } from '@/utils/result.js' export let pageUserRankBy = function(params) { return request.get('/api/community/users/rank/byColumn', { params: params }); } ================================================ FILE: vue_acimage_web/src/api/category.js ================================================ import request from '@/utils/request.js' import axios from 'axios' import MessageUtils from '@/utils/MessageUtils' import global from '@/utils/global.js' export let queryAllCategories = function() { return request.get('/api/community/categories/query/all') } ================================================ FILE: vue_acimage_web/src/api/comment.js ================================================ import request from '@/utils/request.js' //操作 export let addComment = function(data) { return request.post('/api/community/comments/operate', data); } export let deleteComment = function(commentId) { return request.delete('/api/community/comments/operate/' + commentId); } export let modifyComment = function(data) { return request.put('/api/community/comments/operate', data); } //查询 export let queryCommentPage = function(topicId, pageNo) { let reqParams = { topicId: topicId, pageNo: pageNo }; let config = { params: reqParams }; return request.get('/api/community/comments/query/topicComments', config) } export let pageMyComments = function(pageNo) { return request.get('/api/community/comments/query/mine/' + pageNo) } ================================================ FILE: vue_acimage_web/src/api/image.js ================================================ import request from '@/utils/request.js' import axios from 'axios' import MessageUtils from '@/utils/MessageUtils' import global from '@/utils/global.js' export let downloadImages = function(imageIds, fileName) { let urlParams = new URLSearchParams(); for (let imageId of imageIds) { urlParams.append("imageIds", imageId); } // let url='/api/files/download/imageFiles?'+urlParams; let config = { responseType: 'blob', params: urlParams, headers: { 'authorization': global.TOKEN() } } axios.post('/api/image/download/imageFiles', {}, config) .then(resp => { if (resp.data.type == 'application/json') { const reader = new FileReader(); //创建一个FileReader实例 reader.readAsText(resp.data, 'utf-8'); //读取文件,结果用字符串形式表示 reader.onload = function() { //读取完成后,**获取reader.result** const result = JSON.parse(reader.result); MessageUtils.notice(result.msg); } return; } let url = window.URL.createObjectURL(new Blob([resp.data])); let link = document.createElement('a'); link.style.display = 'none'; link.href = url; link.setAttribute('download', fileName + ".zip"); document.body.appendChild(link); link.click(); // 释放URL对象所占资源 window.URL.revokeObjectURL(url); // 用完即删 document.body.removeChild(link); }) } export let searchImagesByImage = function(reqData) { let config = { 'Content-type': 'multipart/form-data' }; return request.post('/api/image/images/searchByImage', reqData, config); } export let uploadTopicImage = function(reqData) { let config = { 'Content-type': 'multipart/form-data' }; return request.post('/api/image/images/operate/upload/topicImage', reqData, config); } ================================================ FILE: vue_acimage_web/src/api/login.js ================================================ import request from '@/utils/request.js' import axios from 'axios' import { Code } from '@/utils/result.js' export let sendCodeToEmail = function(email) { let param = { email: email } return request.post('/api/user/logins/sendCode',{},{ params: param }) } export let getPublicKey = function() { return request.get('/api/user/logins/getPublicKey') } export let queryIsUsernameExist = function(username) { return axios.get('/api/user/users/isExist/' + username).then(resp => { let res = resp.data; if (res.code == Code.OK) { return res; } }); } export let doLogin = function(data) { return request.post('/api/user/logins/doLogin', data) } export let doRegister = function(data) { return request.post('/api/user/logins/doRegister', data) } export let doLogout = function() { return request.post('/api/user/logins/logout'); } ================================================ FILE: vue_acimage_web/src/api/message.js ================================================ import request from '@/utils/request.js' //查询 export let pageCommentMessages = function(pageNo,pageSize) { return request.get('/api/user/messages/query/comments/page/'+pageNo+'/'+pageSize) } ================================================ FILE: vue_acimage_web/src/api/photo.js ================================================ import request from '@/utils/request.js' import axios from 'axios' import MessageUtils from '@/utils/MessageUtils' import global from '@/utils/global.js' export let uploadPhoto = function(reqData) { let config = { 'Content-type': 'multipart/form-data' }; return request.post('/api/image/photos/operate/upload', reqData, config); } ================================================ FILE: vue_acimage_web/src/api/star.js ================================================ import request from '@/utils/request.js' //操作 export let deleteStar=function(topicId){ return request.delete('/api/community/stars/operate/'+topicId); } export let addStar=function(topicId){ return request.post('/api/community/stars/operate/'+topicId); } //查询 export let queryIsStar=function(topicId){ return request.get('/api/community/stars/query/isStar/'+topicId) } export let pageMyStar=function(pageNo){ return request.get('/api/community/stars/query/mine/' + pageNo); } ================================================ FILE: vue_acimage_web/src/api/tag.js ================================================ import request from '@/utils/request.js' import axios from 'axios' import MessageUtils from '@/utils/MessageUtils' import global from '@/utils/global.js' export let queryAllTags = function() { return request.get('/api/community/tags/query/all') } ================================================ FILE: vue_acimage_web/src/api/topic.js ================================================ import request from '@/utils/request.js' //查询话题 export let queryRecentHotTopics = function() { return request.get('/api/community/topics/query/recentHot'); } export let queryRecommendTopics = function() { return request.get('/api/community/topics/query/recommend'); } export let queryMostCommentCountTopics = function() { return request.get('/api/community/topics/query/most/commentCount'); } export let queryTopicAndFirstCommentPage = function(topicId) { return request.get('/api/community/topics/query/info/' + topicId); } export let pageMyPublishTopic = function(pageNo) { return request.get('/api/community/topics/query/mine/' + pageNo); } export let pageActiveTopics = function(pageNo,pageSize) { return request.get('/api/community/topics/query/most/active/' + pageNo+'/'+pageSize) } export let get3HotTopicLists = function() { return request.get('/api/community/topics/query/hot/3attrs') } // export let pageByCategoryId = function(formData) { return request.get('/api/community/topics/query/byCategoryId',{params:formData}) } export let pageByTagId = function(formData) { return request.get('/api/community/topics/query/byTagId',{params:formData}) } export let pageBySort = function(formData) { return request.get('/api/community/topics/query/bySort',{params:formData}) } //操作话题 export let addTopic = function(formData) { let config = { 'Content-type': 'multipart/form-data' }; return request.post('/api/community/topics/operate', formData, config); } export let modifyTitle = function(id, title) { return request.put('/api/community/topics/operate/title/' + id + '/' + title); } export let modifyHtml= function(data) { return request.put('/api/community/topics/operate/html', data); } export let deleteTopic = function(topicId) { return request.delete('/api/community/topics/operate/' + topicId) } ================================================ FILE: vue_acimage_web/src/api/user.js ================================================ import request from '@/utils/request.js' import axios from 'axios' import { Code } from '@/utils/result.js' //查询 export let queryProfile = function() { return request.get('/api/user/users/query/me'); } //操作 export let modifyUsername = function(newUsername) { return request.put('/api/user/users/operate/username/' + newUsername) } // export let uploadPhoto = function(reqData) { // let config = { 'Content-type': 'multipart/form-data' }; // return request.post('/api/user/users/uploadPhoto', reqData, config); // } ================================================ FILE: vue_acimage_web/src/components/BaseContainer/BaseContainer.css ================================================ .container{ display:inline-block; width: 100%; } .header{ position:relative; height:60px; } .header-icon{ font-size:40px; color:#E5E544; } .header-title{ position:relative; top:-6px; display: inline-block; font-size:25px; color:#999999; } ================================================ FILE: vue_acimage_web/src/components/BaseContainer/BaseContainer.vue ================================================ ================================================ FILE: vue_acimage_web/src/components/CategoryCard/CategoryCard.css ================================================ .categories-container>>>.el-button { display: inline-block; margin-top: 6px; margin-right: 4px; margin-left: auto; } .category-card-div>>> .el-card{ border-width:2px; } .categories-container { padding-bottom: 10px; } ================================================ FILE: vue_acimage_web/src/components/CategoryCard/CategoryCard.vue ================================================ ================================================ FILE: vue_acimage_web/src/components/EditBoard/EditBoard.css ================================================ .wrapper{ } ================================================ FILE: vue_acimage_web/src/components/EditBoard/EditBoard.vue ================================================ ================================================ FILE: vue_acimage_web/src/components/FloatImage/FloatImage.css ================================================ .container{ position:relative; width:100%; height:250px; margin-bottom: 15px; display: inline-block; background-color:white; border-radius:5px 5px; box-shadow:1px 1px 1px #EDEDE4; color:#7D7D70; } .container:hover{ box-shadow:0px 0px 6px #626258 !important; transform: translate(0, -3px); transition:transform 0.2s; } .title{ width:100%; word-wrap: break-word; font-size: 14px; position:absolute; z-index:11; bottom:50px; color:white; background-image: linear-gradient(to top, rgba(0, 0, 0, 0.7), rgba(99, 99, 99, 0.01)); /* rgba(99,99,99,1); */ } /* .scan-in-title{ display:inline; font-size:8px; margin-lef:5px; } */ .image{ width:100%; height:200px; border-radius:5px 5px 0px 0px; } .info{ position:absolute; left:10px; top:205px; width:95%; } .username{ position: absolute; font-size: 10px; top:2px; left:24px; } .time{ font-size:10px; margin-left:3px; display: inline-block; width:50% } .time-icon-color{ color:red; font-weight:bold !important; } .info-star{ margin-top:-4px; display: inline-block; } .info-star-icon{ position: relative; top:0.8px; display: inline-block; } .info-star-num{ display: inline-block; } ================================================ FILE: vue_acimage_web/src/components/FloatImage/FloatImage.vue ================================================ ================================================ FILE: vue_acimage_web/src/components/HelloWorld.vue ================================================ ================================================ FILE: vue_acimage_web/src/components/HomeCarousel/HomeCarousel.vue ================================================ ================================================ FILE: vue_acimage_web/src/components/MaskImage/MaskImage.css ================================================ .mask-image-container { width: 250px; height: 175px; margin-left: 10px; position: relative; display: inline-block; border-radius: 5px; overflow: hidden; } .image-container { width: 250px; height: 150px; border-radius: 5px; overflow: hidden; } .image-mask { width: 250px; height: 150px; position: absolute; top: 0px; left: 0px; border-radius: 5px; overflow: hidden; background-color: rgba(169, 165, 164, 0.4); z-index: 11; color: white; opacity: 0; } .image-mask:hover { opacity: 0.9; transition: opacity 0.4s; } /* .opacity-gradient{ opacity: 0; } */ /* .opacity-gradient:hover { opacity: 0.9; transition: opacity 0.4s; } */ .image-info { width: 230px; margin-left: 10px; margin-right: 10px; margin-top: 10px; } .image-info-bottom { position: absolute; bottom: 10px; font-family: 'Times New Roman', Times, serif; font-size: 16px; } .bottom-title{ color: #555555; font-size: 14px; display:inline-block; vertical-align: top; } ================================================ FILE: vue_acimage_web/src/components/MaskImage/MaskImage.vue ================================================ ================================================ FILE: vue_acimage_web/src/components/MyHeader/MyHeader.css ================================================ /* tabs */ .tabs-wrapper { height: 60px; font-size: 22px; background-color: white; box-shadow: 0px 0px 2px #A7A796; text-align: center; position: relative; } .tabs-wrapper>img:last-child { height: 55px; position: absolute; right: 10px; } .tabs-wrapper>img:first-child { height: 55px; position: absolute; left: 10px; } .tabs-container { display: inline-block; margin-top: 16px; font-family: serif; } .tabs-container>div { display: inline-block; } .tabs-item-link { color: #666666; font-size: 22px; } .tabs-item-link:hover { color: #0074A0; } /* End tabs */ ================================================ FILE: vue_acimage_web/src/components/MyHeader/MyHeader.vue ================================================ ================================================ FILE: vue_acimage_web/src/components/MyHeader/MyNavigation/MyNavigation.vue ================================================ ================================================ FILE: vue_acimage_web/src/components/ShowTinymce/ShowTinymce.vue ================================================ ================================================ FILE: vue_acimage_web/src/components/TagCard/TagCard.css ================================================ .tags-container>>>.el-tag:hover { border: 1px solid skyblue; } .tags-container>>>.el-tag:active { border: 1px solid blue; } .tags-container>>>.el-tag:focus { border: 1px solid blue !important; } .tags-container>>>.el-tag { display: inline-block; margin-top: 6px; margin-right: 5px; margin-left: auto; } ================================================ FILE: vue_acimage_web/src/components/TagCard/TagCard.vue ================================================ ================================================ FILE: vue_acimage_web/src/components/TopicCard/TopicCard.css ================================================ .wrapper { width: 760px; height: 130px; display: inline-block; position: relative; border-radius: 3px; box-shadow: 1px 1px 6px #CCCCCC; background-color: white; } .wrapper:hover { box-shadow: 1px 1px 6px #AAAAAA; transform: translate(0, -2px); transition: transform 0.2s; } .wrapper-left { height: 100%; width: 135px; display: inline-block; } /* .wrapper-left>img { height: 100%; width: 120px; object-fit: cover; border-radius: 3px; display: inline-block; } */ .wrapper-left>>>.el-image { height: 100%; width: 120px; object-fit: cover; border-radius: 3px; display: inline-block; } .white-gradient { position: absolute; left: 61px; top: 0px; height: 100%; width: 60px; background: linear-gradient(to left, #fff, rgba(255, 255, 255, 0)); } .wrapper-medium { position: relative; display: inline-block; vertical-align: top; width: 475px; height: 100%; } .title { font-size: 16px; margin-top: 5px; width: 475px; height: 20px; overflow: hidden; color: #333333; } .content { font-size: 14px; color: #999999; height: 58px; overflow: hidden; margin-top: 8px; } .info { position: absolute; bottom: 5px; left: 10px; } .username { display: inline-block; position: relative; bottom: 5px; /* margin-bottom: auto; */ color: #666666; margin-left: 5px; font-size: 14px; } .data { display: inline-block; position: relative; bottom: 5px; /* margin-bottom: auto; */ margin-left: 5px; font-size: 12px; color: #999999; } .wrapper-right { position: relative; display: inline-block; vertical-align: top; height: 100%; width: 140px; padding-left: 10px; } .category-container { margin-top: 10px; height: 30px; } .tags-container { margin-top: 10px; } .tags-container>>>.el-tag { margin-top: 3px; margin-right: 3px; } ================================================ FILE: vue_acimage_web/src/components/TopicCard/TopicCard.vue ================================================ ================================================ FILE: vue_acimage_web/src/components/TopicList/TopicList.css ================================================ .categories-container>>>.el-button { display: inline-block; margin-top: 6px; margin-right: 4px; margin-left: auto; } .category-card-div>>> .el-card{ border-width:2px; } .categories-container { padding-bottom: 10px; } .icon-medal{ position: relative; font-size: 20px; top:4px; } .data-container{ margin-left: 20px; margin-bottom: -5px; font-size: 12px; } .item-container{ font-size: 14px; position: relative; margin-bottom: 5px; } .ml7{ margin-left: 7px; } ================================================ FILE: vue_acimage_web/src/components/TopicList/TopicList.vue ================================================ ================================================ FILE: vue_acimage_web/src/components/TopicRank/TopicRank.css ================================================ .wrapper{ box-shadow:0px 0px 5px #CCCCCC; border-radius: 3px; } .header{ position: relative; text-align: center; border-radius: 4px 4px 0px 0px; overflow: hidden; height:134px; } .header img { width: 100%; height: 100px; border-radius: 3px; object-fit: cover; z-index: 5; } .white-gradient { position: absolute; top: 20px; height: 80px; width: 100%; z-index:8; background: linear-gradient(to top, rgba(255, 255, 255, 0.92),rgba(255, 255, 255, 0.6), rgba(255, 255, 255, 0)); } .header-item{ height: 34px; background-color: #CCCCCC; display: inline-block; text-align: center; color:white; } .label{ margin-top:5px; font-size: 16px; } .body{ padding-left:20px; padding-bottom: 10px; padding-top:15px; background-color: white; } .image-container{ width:90px; height:60px; border-radius: 5px; position:relative; display:inline-block; } .image-container>>>.el-image{ border-radius: 4px; width:90px; height:60px } .image-index{ background-color:#FF6C94; position: absolute; top:0px; } .index{ width:18px; height:19px; color:white; text-align: center; border-radius: 2px; } .front-title{ display: inline-block; overflow: hidden; vertical-align: top; margin-left: 5px; width: calc(100% - 100px) } .hover-bg-red:hover{ background-color:#FF566A !important; } ================================================ FILE: vue_acimage_web/src/components/TopicRank/TopicRank.vue ================================================ ================================================ FILE: vue_acimage_web/src/config.js ================================================ export default{ domainOfImages:'http://rof8epeiz.hn-bkt.clouddn.com/', } ================================================ FILE: vue_acimage_web/src/main.js ================================================ import Vue from 'vue' import App from './App.vue' import router from '@/router' import store from '@/store' import axios from 'axios' Vue.prototype.axios = axios import ElementUI from 'element-ui'; import 'element-ui/lib/theme-chalk/index.css'; Vue.use(ElementUI); import {Notification} from 'element-ui'; Vue.prototype.$notify = Notification; import global from '@/utils/global.js'; Vue.prototype.$global=global //防xss攻击 import VueDOMPurifyHTML from 'vue-dompurify-html' Vue.use(VueDOMPurifyHTML) Vue.config.productionTip = false if(process.env.VUE_APP_MOCK){ require('../mock') } new Vue({ render: h => h(App), router, store }).$mount('#app') ================================================ FILE: vue_acimage_web/src/router/index.js ================================================ import Vue from 'vue' import VueRouter from 'vue-router' Vue.use(VueRouter) import Home from '@/views/Home/Home.vue' import Login from '@/views/Login/Login.vue' import PublishTopic from '@/views/PublishTopic/PublishTopic.vue' import MyProfile from '@/views/MyProfile/MyProfile.vue' import MyActivity from '@/views/MyActivity/MyActivity.vue' import MyMessage from '@/views/MyMessage/MyMessage.vue' import Forum from '@/views/Forum/Forum.vue' import About from '@/views/About/About.vue' import SearchImage from '@/views/SearchImage/SearchImage.vue' import SearchTopic from '@/views/SearchTopic/SearchTopic.vue' const router = new VueRouter({ routes: [ { path: '/', component: Home }, { path: '/login', component: Login }, { path: '/publish', component: PublishTopic }, { path: '/topic/:id', component: ()=>import('@/views/TopicInfo/TopicInfo.vue') }, { path: '/profile', component: MyProfile }, { path: '/MyActivity', component: MyActivity }, { path: '/MyMessage', component: MyMessage }, { path: '/forum', component: Forum }, { path: '/SearchTopic', component: SearchTopic }, { path: '/SearchImage', component: SearchImage }, { path: '/about', component: About }, ] }) export default router ================================================ FILE: vue_acimage_web/src/store/index.js ================================================ import Vuex from 'vuex' import Vue from 'vue' import Config from '@/config.js' import CommonUtils from '@/utils/CommonUtils.js' import jwtDecode from "jwt-decode"; import { queryAllCategories } from "@/api/category.js" import { queryAllTags } from "@/api/tag.js" import { Code } from '@/utils/result.js' //应用vuex插件 Vue.use(Vuex) export default new Vuex.Store({ //数据,相当于data state: { userId: '', username: '', photoUrl: '', token: '', categoryList: [], tagList: [], messageNum: null, hasWebSocket:false, //elementui五种按钮类型 // types: ['success', 'primary', 'info', 'warning', 'danger'] }, getters: { getPhotoUrl(state) { if (!CommonUtils.isEmpty(state.photoUrl)) { return state.photoUrl; } return 'static/image/default-photo.webp'; }, categoryLabel(state) { return (id) => { for (let item of state.categoryList) { if (item.id == id) { return item.label; } } return null; } }, tagLabel(state) { return (id) => { for (let item of state.tagList) { if (item.id == id) { return item.label; } } return null; } }, isLogin(state) { let token = localStorage.getItem("token"); return !CommonUtils.isEmpty(token); } }, //里面定义方法,操作state方发 mutations: { init(state) { state.token = localStorage.getItem("token"); try { state.userId = jwtDecode(state.token).userId; state.userName = jwtDecode(state.token).userName; state.photoUrl = jwtDecode(state.token).photoUrl; } catch (err) { state.userId = ''; state.username = ''; state.photoUrl = ''; } //查询分类 queryAllCategories().then(res => { if (res.code == Code.OK) { state.categoryList = res.data; } }); //查询标签 queryAllTags().then(res => { state.tagList = res.data; }); }, setToken(state, token) { localStorage.setItem("token", token); this.commit('init') }, removeToken(state) { localStorage.removeItem("token"); this.commit('init') } }, // 操作异步操作mutation actions: { }, modules: { }, }) ================================================ FILE: vue_acimage_web/src/utils/CommonUtils.js ================================================ import MessageUtils from '@/utils/MessageUtils' import { Code } from "@/utils/result.js" export default { isEmpty(object) { if (object == undefined || object == '' || object == null || object == {} || object == []) { return true; } return false; }, popMsgAndRefreshIfOk(request, msg, delaySeconds) { request.then(result => { console.log(result); if (result.code == Code.OK) { MessageUtils.success(msg); if (!this.isEmpty(delaySeconds)) { this.delayRefresh(delaySeconds); } } }); }, popMsgIfOk(request, msg) { this.popMsgAndRefreshIfOk(request, msg); }, delayRefresh(delaySeconds) { setTimeout(() => { location.reload(); }, delaySeconds * 1000); }, getUrlParams(url) { let firstIndex = url.indexOf('?'); if (firstIndex == -1) return; let argStr = url.slice(firstIndex + 1, url.length); let args = argStr.split("&"); let params = {}; for (let i = 0; i < args.length; i++) { let keyAndValues = args[i].split("="); params[keyAndValues[0]] = keyAndValues[1]; } return params; }, toFormData(object) { const formData = new FormData() Object.keys(object).forEach(key => { const value = object[key] if (Array.isArray(value)) { value.forEach((subValue, i) => formData.append(key + `[${i}]`, subValue) ) } else { formData.append(key, object[key]) } }) return formData }, copyPropertiesTo(source,target){ Object.keys(source).forEach(key => { const value = source[key] if(target[key]!=undefined){ target[key]=value } }) } } ================================================ FILE: vue_acimage_web/src/utils/MessageUtils.js ================================================ import { Message, MessageBox, Loading, Notification } from 'element-ui'; export default{ notice(msg,durationSeconds,type){ let newType=type||'warning'; let newDuration=durationSeconds||4; Notification({ title: '提示', message: msg, type: newType, duration: newDuration*1000 }); }, success(msg,durationSeconds){ let newDuration=durationSeconds||4; Notification({ title: '成功', message: msg, type: 'success', duration: newDuration*1000 }); }, confirm(msg){ return MessageBox.confirm(msg, '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }) } } ================================================ FILE: vue_acimage_web/src/utils/StringUtils.js ================================================ export default { html2Text(htmlString) { return htmlString.toString().replace(/<(style|script|iframe)[^>]*?>[\s\S]+?<\/\1\s*>/gi, '') .replace(/<[^>]+?>/g, '') .replace(/\s+/g, ' ') .replace(/ /g, ' ') .replace(/>/g, ' ') .replace(/ /gm,' '); } } ================================================ FILE: vue_acimage_web/src/utils/global.js ================================================ import Vue from 'vue' import cookie from 'vue-cookie' import CommonUtils from '@/utils/CommonUtils.js' import MessageUtils from '@/utils/MessageUtils.js' import StringUtls from '@/utils/StringUtils.js' import Config from '@/config.js' import { Code } from '@/utils/result.js' import { MessageBox } from 'element-ui' import jwtDecode from "jwt-decode"; import StringUtils from '@/utils/StringUtils.js' let global = new Vue(); // global.trueImageUrl = function(url) { // if (CommonUtils.isEmpty(url) || url == '#') { // return '#'; // } // return global.baseImageUrl + url; // } //获取头像路径 global.getPhotoUrl = function(url) { if (CommonUtils.isEmpty(url) || url == '#') { return 'static/image/default-photo.webp'; } return url; } //获取分享链接 global.getTopicUrl = function(topicId) { return '/topic/' + topicId; } global.isEmpty = function(object) { return CommonUtils.isEmpty(object); } global.buttonType = function(id) { let types = ['success', 'primary', 'warning', 'danger']; return types[id % 4]; } //限制显示长度 global.omitStr = function(content, totalLength) { if (content.length > totalLength) { return content.slice(0, totalLength - 2) + '...' } else { return content; } } global.timeView = function(datetime) { let d1 = new Date(datetime); let d2 = new Date(); let duration = d2.getTime() - d1.getTime(); let time = parseInt(Math.floor(duration / (1000 * 60 * 60 * 24 * 30))); if (time >= 1) { return d1.getFullYear()+'-'+(d1.getMonth()+1)+'-'+d1.getDate(); } time = parseInt(Math.floor(duration / (1000 * 60 * 60 * 24))); if (time == 1) { return '昨天'; } else if (time > 1) { return time + '天前'; } time = parseInt(Math.floor(duration / (1000 * 60 * 60))); if (time >= 1) { return time + '小时前' } time = parseInt(Math.floor(duration / (1000 * 60))); if (time >= 1) { return time + '分钟前' } time = parseInt(Math.floor(duration / (1000))); if (time >= 1) { return time + '秒前' } return '刚刚' } global.html2Text = function(contentHtml, limitLength) { let content = StringUtils.html2Text(contentHtml); if (limitLength == undefined) { return content; } if (content.length > limitLength) { return content.slice(0, limitLength - 3) + '...' } else { return content; } } export default global; ================================================ FILE: vue_acimage_web/src/utils/request.js ================================================ import axios from 'axios'; import { Message, MessageBox, Loading, Notification } from 'element-ui'; import MessageUtils from '@/utils/MessageUtils' import { Code } from '@/utils/result.js'; import store from '@/store' // const service = axios.create({ // baseURL: "http://127.0.0.1:8080/projectName",//请求地址前缀 // timeout: 0 // }); const service = axios.create(); var requestNum = 0; var loading = null; // 请求拦截器 service.interceptors.request.use( config => { //添加请求头部参数 // config.headers['arg1'] = "arg1Value"; //开始loading config.headers['authorization'] = store.state.token; requestNum++; if (loading == null) { loading = Loading.service({ fullscreen: true, text: '正在努力加载中~', background: 'rgba(250, 250, 250, 0.5)' }); // setTimeout(() => { loading.close(); }, 1); } else if (loading != null && requestNum > 0) { loading = Loading.service({ fullscreen: true, text: '正在努力加载中~',background: 'rgba(250, 250, 250, 0.5)' }); } return config; }, error => { requestNum = 0; if (loading) { loading.close(); } return Promise.reject(error); } ); // 响应拦截器 service.interceptors.response.use( response => { //拦截到成功的数据 requestNum--; if (loading == null || requestNum <= 0) { loading.close() } const result = response.data; if (result.code == Code.ERR) { MessageUtils.notice(result.msg, 2) } else if (result.code == Code.OK) { return response.data; } else { // 出错了直接关闭loading requestNum = 0 loading.close(); } }, error => { //拦截到失败的数据 console.log('错误码', error) if (error.response.status == 401) { MessageUtils.notice('( ̄_, ̄ )请登录或成为认证用户后再操作') } else if(error.response.status == 429){ MessageUtils.notice('系统繁忙┗( T﹏T )┛') } else { MessageUtils.notice("出错了o(≧口≦)o,状态码"+error.response.status) } // 出错了直接关闭loading requestNum = 0; loading.close(); return Promise.reject(error); } ); export default service; ================================================ FILE: vue_acimage_web/src/utils/result.js ================================================ // 状态码 export let Code = { OK: 20000, ERR: 20001, DATA_NOT_EXIST: 20011 } ================================================ FILE: vue_acimage_web/src/utils/utils.js ================================================ ================================================ FILE: vue_acimage_web/src/views/About/About.css ================================================ body{ background-color: #F5F5F5; margin:0px; padding:0px; } .upload-plus-disabled .el-upload--picture-card { display: none; } .main{ width:1040px; margin-left: calc( (100% - 510px - 520px) / 2 ); margin-top: 15px; display: block; background-color: white; border-radius: 5px; box-shadow:0px 0px 5px #CCCCCC; padding-bottom: 200px; } .header{ top:5px; position:relative; height:60px; text-align: center; } .header-title{ position:relative; top:-6px; display: inline-block; font-size:25px; color:#999999; } .header-icon{ font-size:40px; color:#E5E544; } .upload-container{ width:400px; margin-left: calc(50% - 400px / 2); } .upload-hint{ color:red; font-size:15px; font-family:'Times New Roman', Times, serif; margin-bottom:5px; } .search-result-container{ margin-top:20px; margin-left:100px; margin-right:100px; } .result-image-container{ margin-right:10px; height:250px; width:150px; display:inline-block; } ================================================ FILE: vue_acimage_web/src/views/About/About.vue ================================================ ================================================ FILE: vue_acimage_web/src/views/Forum/Forum.css ================================================ .forum-div { /* background-color: #FBFBFB; */ background-color: #F9FBFB; margin: 0px; padding: 0px; } .upload-plus-disabled .el-upload--picture-card { display: none; } .wrapper { width: 1130px; margin-left: calc((100% - 1120px) / 2); margin-top: 15px; display: block; padding-bottom: 100px; border-radius: 5px; } .wrapper-left { width: 760px; display: inline-block; /* background-color: white; */ background-color: rgba(0, 0, 0, 0); /* border-radius: 5px; box-shadow:0px 0px 5px #CCCCCC; */ padding-bottom: 10px; } .wrapper-header { top: 5px; position: relative; border-radius: 5px; /* height: 44px; */ height: 350px; /* text-align: center; */ margin-bottom: 20px; } .mask-images-container{ width:520px; margin-left:10px; display: inline-block; vertical-align: top; } .mask-images-container>div{ display: inline-block; } .wrapper-header-icon { font-size: 40px; color: #E5E544; } .wrapper-header-title { position: relative; top: -6px; display: inline-block; font-size: 25px; color: #999999; } .wrapper-right { width: 340px; display: inline-block; vertical-align: top; /* border-radius: 5px; box-shadow: 0px 0px 5px #CCCCCC; */ background-color: rgba(0, 0, 0, 0); /* padding-bottom: 200px; */ margin-left: 30px; } .mt10 { margin-top: 10px; } /* .user-rank-header { font-size: 22px; color: skyblue; text-align: center; margin-top: 10px; } .topic-item { margin-bottom: -20px; margin-top: -20px; padding-left: 10px; padding-right: 10px; } .item-medium { display: inline-block; vertical-align: top; margin-left: 10px; width: 500px; } .topic-title { color: #666666; font-size: 18px; } .topic-data { color: #999999; font-size: 14px; } .topic-content { margin-top: 2px; color: #999999; font-size: 16px; } .item-right { display: inline-block; vertical-align: top; } .item-right-image { height: 100px; width: 100px; border-radius: 3px; margin-top: 4px; } */ ================================================ FILE: vue_acimage_web/src/views/Forum/Forum.vue ================================================ ================================================ FILE: vue_acimage_web/src/views/Home/Home.css ================================================ .home-wrapper{ background-color: white; padding-bottom: 200px; } .home-main{ width:1130px; margin-left: calc( (100% - 1120px) / 2 ); margin-top: 20px; display: block; } .mask-images-container{ width:520px; margin-left:10px; display: inline-block; vertical-align: top; } .mask-images-container>div{ display: inline-block; } .float-image-container{ display: inline-block; margin-left: 18px; margin-right:18px; width: 190px; } ================================================ FILE: vue_acimage_web/src/views/Home/Home.vue ================================================ ================================================ FILE: vue_acimage_web/src/views/Home/UserRank/UserRank.css ================================================ .wrapper { width: 100%; background-color: white; border-radius: 5px; box-shadow: 0px 0px 5px #CCCCCC; padding-bottom: 40px; position: relative; transition: 0.4s; } .wrapper:hover { box-shadow: 0px 0px 5px #AAAAAA; transition: 0.5s; } .wrapper-header>img { width: 100%; height: 100px; border-radius: 3px; object-fit: cover; z-index: 5; } .white-gradient { position: absolute; top: 45px; height: 60px; width: 100%; background: linear-gradient(to top, #fff, rgba(255, 255, 255, 0)); /* background-image: linear-gradient(to top, rgba(255, 255, 255, 1), rgba(255, 255, 255, 0.01)); */ } .header-title { margin-left: 120px; position: absolute; bottom: 12px; font-style: italic; text-shadow: 2px 5px 4px pink; color: rgb(255, 0, 153); } .user-rank-header { font-size: 22px; color: #C5A40F; text-align: center; padding-top: 20px; } .sort-choice-container { margin-top: 10px; margin-bottom: 15px; margin-left: 20px; } .sort-hint { font-size: 14px; margin-right: 5px; margin-left: 5px; color: #333333 } .user-item-container { margin-top: 5px; margin-left: 20px; } .user-item-left { display: inline-block; margin-left: 10px; } .user-item-right { display: inline-block; margin-left: 10px; margin-top: -2px; vertical-align: top; } .user-item-right-header { font-size: 14px; color: #666666; } .user-item-right-bottom>div { color: #12BADF; font-size: 12px; display: inline-block; } .user-item-right-bottom>span { font-size: 12px; color: #666666; margin-left: -3px; margin-right: 5px; } .gray-color { color: #666666; } ================================================ FILE: vue_acimage_web/src/views/Home/UserRank/UserRank.vue ================================================ ================================================ FILE: vue_acimage_web/src/views/Login/Login.css ================================================ body{ margin:0px; padding:0px; background-size: cover; } .login-wrapper{ background-size:cover; height:100vh; width: 100vw; } .login-main{ text-align: center; } .login-container { width: 400px; display: inline-block; /* border:1px solid #409EFF; */ border-radius: 10px; background-color: rgba(255,255,255,0.8); position: fixed; left:51vw; top:20vh; } .login-container:hover { background-color: rgba(255,255,255,0.9); } .login-tabs-container{ margin-top: -20px; margin-bottom: -10px; } .login-tabs-content{ font-size:18px !important; background-color: rgba(0,0,0,0) !important; } .hint-container{ height:22px; display: block; margin-top:10px ; text-align: left; } .hint-content{ color:red; font-size:10px; font-family:'Times New Roman', Times, serif; } .content{ margin-top: -10px; } .content>>>.el-row{ margin-top:10px; } .verify-code{ width: 100px; margin-left:20px; height:40px; display:inline-block; vertical-align: top; } ================================================ FILE: vue_acimage_web/src/views/Login/Login.vue ================================================ ================================================ FILE: vue_acimage_web/src/views/MyActivity/MyActivity.css ================================================ .outer-div { background-color: #F5F5F5; } .wrapper { width: 800px; margin-left: calc((100% - 800px) / 2); margin-top: 15px; display: block; } .wrapper-left { position: relative; width: 100px; display: inline-block; background-color: rgba(255, 255, 255, 0.8); border-radius: 5px; box-shadow: 0px 0px 5px #CCCCCC; padding-bottom: 525px; text-align: center; } .wrapper-left div { font-size: 16px; margin-top: 20px; color: #666666; } .wrapper-left div:nth-child(1) { color: #666666 !important; cursor: auto !important; } .wrapper-left div:hover { color: #0B8FFF; cursor: pointer; } .wrapper-right { width: 680px; vertical-align: top; margin-left: 10px; display: inline-block; } .wrapper-right>div { border-radius: 5px; background-color: rgba(255, 255, 255, 0.9); box-shadow: 0px 0px 5px #CCCCCC; } .activity-header { padding: 15px; font-size: 22px; text-align: center; } .activity-content { position: relative; padding: 10px; margin-top: 10px; padding-bottom: 20px; } .activity-medium { display: inline-block; vertical-align: top; margin-left: 10px; width: 300px; } .activity-time { margin-top: 5px; color: #999999; font-size: 14px; } .activity-right { display: inline-block; vertical-align: top; float: right; } ================================================ FILE: vue_acimage_web/src/views/MyActivity/MyActivity.vue ================================================ ================================================ FILE: vue_acimage_web/src/views/MyActivity/MyCommentActivity/MyCommentActivity.css ================================================ .activity-content{ position: relative; padding:10px; margin-top: 10px; padding-bottom: 20px; } .activity-title{ font-size: 16px; margin-top:-5px; } .activity-title-label{ color:#AAAAAA; font-size:15px; } .activity-medium{ display:inline-block; vertical-align: top; margin-left:10px; width:500px; } .activity-time{ margin-top:5px; color:#999999; font-size:16px; } .activity-right{ display:inline-block; vertical-align: top; float:right; } ================================================ FILE: vue_acimage_web/src/views/MyActivity/MyCommentActivity/MyCommentActivity.vue ================================================ ================================================ FILE: vue_acimage_web/src/views/MyActivity/MyStarActivity/MyStarActivity.css ================================================ .activity-content{ position: relative; padding:10px; margin-top: 10px; padding-bottom: 20px; } .activity-title{ font-size: 16px; margin-top:-5px; } .activity-title-label{ color:#AAAAAA; font-size:15px; } .activity-medium{ display:inline-block; vertical-align: top; margin-left:10px; width:300px; } .activity-time{ margin-top:5px; color:#999999; font-size:16px; } .activity-right{ display:inline-block; vertical-align: top; float:right; } ================================================ FILE: vue_acimage_web/src/views/MyActivity/MyStarActivity/MyStarActivity.vue ================================================ ================================================ FILE: vue_acimage_web/src/views/MyActivity/MyTopicActivity/MyTopicActivity.css ================================================ .activity-content{ position: relative; padding:10px; margin-top: 10px; padding-bottom: 20px; } .activity-title{ font-size: 16px; margin-top:-5px; } .topic-data{ color:#999999; } .activity-title-label{ color:#AAAAAA; font-size:15px; } .activity-medium{ display:inline-block; vertical-align: top; margin-left:10px; width:300px; } .activity-time{ margin-top:5px; color:#999999; font-size:16px; } .activity-right{ display:inline-block; vertical-align: top; float:right; } ================================================ FILE: vue_acimage_web/src/views/MyActivity/MyTopicActivity/MyTopicActivity.vue ================================================ ================================================ FILE: vue_acimage_web/src/views/MyMessage/CommentMessage/CommentMessage.css ================================================ .activity-content{ position: relative; padding:10px; margin-top: 10px; padding-bottom: 20px; } .activity-title{ font-size: 16px; margin-top:-5px; } .activity-title-label{ color:#AAAAAA; font-size:15px; } .activity-medium{ display:inline-block; vertical-align: top; margin-left:10px; width:500px; } .activity-time{ margin-top:5px; color:#999999; font-size:16px; } .activity-right{ display:inline-block; vertical-align: top; float:right; } ================================================ FILE: vue_acimage_web/src/views/MyMessage/CommentMessage/CommentMessage.vue ================================================ ================================================ FILE: vue_acimage_web/src/views/MyMessage/MyMessage.css ================================================ .outer-div { background-color: #F5F5F5; } .wrapper { width: 800px; margin-left: calc((100% - 800px) / 2); margin-top: 15px; display: block; } .wrapper-left { position: relative; width: 100px; display: inline-block; background-color: rgba(255, 255, 255, 0.8); border-radius: 5px; box-shadow: 0px 0px 5px #CCCCCC; padding-bottom: 525px; text-align: center; } .wrapper-left div { font-size: 16px; margin-top: 20px; color: #666666; } .wrapper-left div:nth-child(1) { color: #666666 !important; cursor: auto !important; } .wrapper-left div:hover { color: #0B8FFF; cursor: pointer; } .wrapper-right { width: 680px; vertical-align: top; margin-left: 10px; display: inline-block; } .wrapper-right>div { border-radius: 5px; background-color: rgba(255, 255, 255, 0.9); box-shadow: 0px 0px 5px #CCCCCC; } .activity-header { padding: 15px; font-size: 22px; text-align: center; } .activity-content { position: relative; padding: 10px; margin-top: 10px; padding-bottom: 20px; } .activity-medium { display: inline-block; vertical-align: top; margin-left: 10px; width: 300px; } .activity-time { margin-top: 5px; color: #999999; font-size: 14px; } .activity-right { display: inline-block; vertical-align: top; float: right; } ================================================ FILE: vue_acimage_web/src/views/MyMessage/MyMessage.vue ================================================ ================================================ FILE: vue_acimage_web/src/views/MyProfile/MyProfile.css ================================================ .wrapper{ width:800px; margin-left: calc( (100% - 800px) / 2 ); margin-top: 15px; display: block; } .main{ position:relative; display: block; background-color: white; border-radius: 5px; box-shadow:0px 0px 5px #CCCCCC; padding-bottom: 100px; } .profile-header{ top:5px; position:relative; height:60px; padding-left: 10px; } .profile-header-title{ position:relative; top:-6px; display: inline-block; font-size:25px; color:#86DAF3; } .profile-header-icon{ font-size:40px; color:#86DAF3; } .profile-content{ position: relative; margin-left:15%; margin-top:15px; } .profile-content-left{ width:120px; text-align: center; display: inline-block; } .profile-content-right{ width:400px; position: relative; margin-left:40px; margin-top:20px; vertical-align: top; display: inline-block; color:#666666; } /* .profile-content-right>div{ height:20px; margin-bottom:10px; } */ .item-edit-btn{ display: inline; position: absolute; z-index: 10; right:0px; margin-top:3px; } ================================================ FILE: vue_acimage_web/src/views/MyProfile/MyProfile.vue ================================================ ================================================ FILE: vue_acimage_web/src/views/PublishTopic/PublishTopic.css ================================================ .wrapper { width: 1200px; margin-left: calc((100% - 1200px) / 2); margin-top: 15px; display: block; background-color: white; border-radius: 5px; box-shadow: 0px 0px 5px #CCCCCC; padding-bottom: 200px; } .wrapper-header { top: 5px; position: relative; height: 60px; text-align: center; margin-bottom: 30px; } .header-title { position: relative; top: -6px; display: inline-block; font-size: 25px; color: #999999; } .header-icon { font-size: 40px; color: #E5E544; } .upload-container { width: 320px; height: 148px; border: 2px solid skyblue; border-radius: 5px; margin-top: 20px; text-align: center; } .upload-hint { color: red; font-size: 15px; font-family: 'Times New Roman', Times, serif; } .cropper-container{ text-align:center; width: 300px; height: 300px; margin-left: 100px; } .mr5 { margin-right: 5px; } .active-blue:hover { background-color: blue !important; transition: 1s; } ================================================ FILE: vue_acimage_web/src/views/PublishTopic/PublishTopic.vue ================================================ ================================================ FILE: vue_acimage_web/src/views/SearchImage/SearchImage.css ================================================ body{ background-color: #F5F5F5; margin:0px; padding:0px; } .upload-plus-disabled .el-upload--picture-card { display: none; } .search-main{ width:1040px; margin-left: calc( (100% - 510px - 520px) / 2 ); margin-top: 15px; display: block; background-color: white; border-radius: 5px; box-shadow:0px 0px 5px #CCCCCC; padding-bottom: 200px; } .search-header{ top:5px; position:relative; height:60px; text-align: center; } .search-header-title{ position:relative; top:-6px; display: inline-block; font-size:25px; color:#999999; } .search-header-icon{ font-size:40px; color:#E5E544; } .upload-container{ width:400px; margin-left: calc(50% - 400px / 2); } .upload-hint{ color:red; font-size:15px; font-family:'Times New Roman', Times, serif; margin-bottom:5px; } .search-result-container{ margin-top:20px; margin-left:100px; margin-right:100px; } .result-image-container{ margin-right:10px; height:250px; width:150px; display:inline-block; } ================================================ FILE: vue_acimage_web/src/views/SearchImage/SearchImage.vue ================================================ ================================================ FILE: vue_acimage_web/src/views/SearchTopic/SearchTopic.css ================================================ .wrapper{ width:1040px; margin-left: calc( (100% - 510px - 520px) / 2 ); margin-top: 15px; display: block; background-color: white; border-radius: 5px; /* box-shadow:0px 0px 5px #CCCCCC; */ padding-bottom: 200px; } .wrapper-top{ padding-top: 20px; padding-bottom: 20px; width:100%; border-radius: 5px; box-shadow:0px 0px 5px #CCCCCC; } .wrapper-header{ top:5px; position:relative; height:60px; text-align: center; margin-bottom: 30px; } .header-title{ position:relative; top:-6px; display: inline-block; font-size:25px; color:#999999; } .header-icon{ font-size:40px; color:#E5E544; } .mr5{ margin-right: 5px; } .active-blue:hover{ background-color:blue !important; transition: 1s; } ================================================ FILE: vue_acimage_web/src/views/SearchTopic/SearchTopic.vue ================================================ ================================================ FILE: vue_acimage_web/src/views/TopicInfo/PublishComment/PublishComment.css ================================================ .user-comment-container{ text-align: center; width:100%; margin-bottom: 20px; } .user-comment-container>div{ display: inline-block; vertical-align: top; } ================================================ FILE: vue_acimage_web/src/views/TopicInfo/PublishComment/PublishComment.vue ================================================ ================================================ FILE: vue_acimage_web/src/views/TopicInfo/TopicInfo.css ================================================ body { background-color: #F5F5F5; margin: 0px; padding: 0px; } .wrapper { width: 1230px; margin-left: calc((100% - 1200px) / 2); margin-top: 15px; display: block; padding-bottom:200px; } .wrapper-left { position: relative; width: 900px; display: inline-block; background-color: white; border-radius: 5px; box-shadow: 0px 0px 5px #CCCCCC; padding-bottom: 200px; overflow: hidden; } .title { text-align: center; font-size: 25px; font-family: 'Times New Roman', Times, serif; font-weight: bold; margin-top: 20px; color: #666666; } .title-edit-button { display: inline-block; float: right; margin-right: 40px; } .topic-info { margin-left: 5%; margin-top: 10px; color: #666666; } .tags-container{ margin-left:10px; } .tags-container>>>.el-tag{ margin-right:3px; } .time-hint-container { position: relative; width: 100%; height: 60px; } .time-hint { margin-top: 30px; margin-left: 40px; color: #999999; } .content-edit-button { display: inline-block; margin-right: 40px; float: right; } .topic-html-container{ width:820px; display: block; margin-left:40px; overflow: hidden; } .comments-container { margin-left: 10%; width: 80%; } .comments-count { height: 40px; color: #333333; margin-left: -3%; font-size: 14px; } .gray-color { color: #AAAAAA; } .wrapper-right { width: 300px; vertical-align: top; margin-left: 20px; display: inline-block; border-radius: 5px; } .user-container{ text-align: center; padding-bottom: 20px; border:2px solid #EEEEEE; border-radius: 5px; transition: 0.3s; /* box-shadow: 0px 0px 5px #CCCCCC; */ } ================================================ FILE: vue_acimage_web/src/views/TopicInfo/TopicInfo.vue ================================================ ================================================ FILE: vue_acimage_web/src/views/TopicInfo/UserComment/UserComment.css ================================================ /*comment组件*/ .comment-container>div { display: inline-block; vertical-align: top; } .comment-right { border-bottom: 1.5px solid #DDDDDD; margin-left: 10px; width: 500px; /* padding-top: 5px; */ /* padding-bottom: 5px; */ } .comment-username { display: block; margin-top: 10px; color: #666666; font-family: 'Times New Roman', Times, serif; font-size: 14px; font-weight: bold; } .comment-content { display: block; padding-top: 10px; /* margin-top:5px; */ font-family: FangSong_GB2312; /* font-weight: bold; */ /* font-family: Tahoma,Helvetica,Arial,'宋体',sans-serif; */ color: #555555; font-size: 15px !important; } .comment-bottom { display: block; position: relative; margin-top: 10px; color: #999999; font-family: 'Times New Roman', Times, serif; } .edit-button { margin-top: -5px; display: inline-block; float: right; } /*End comment组件*/ .gray-color { color: #AAAAAA; } ================================================ FILE: vue_acimage_web/src/views/TopicInfo/UserComment/UserComment.vue ================================================ ================================================ FILE: vue_acimage_web/vue.config.js ================================================ const { defineConfig } = require('@vue/cli-service') module.exports = defineConfig({ transpileDependencies: true }) module.exports = { devServer: { port: 8010, proxy: { "/websocket": { target: "ws://127.0.0.1:81/", // 需要代理访问的api地址 changeOrigin: true, // 允许跨域请求 ws: true }, "/api": { // 代理名称 凡是使用/api开头的地址都是用此代理 target: "http://127.0.0.1:81/", // 需要代理访问的api地址 changeOrigin: true, // 允许跨域请求 // pathRewrite: { // // 重写路径,替换请求地址中的指定路径 // "^/api": "/", // 将请求地址中的/api替换为空,也就是请求地址中不会包含/api/ // }, }, "/storage": { target: "http://127.0.0.1:81/", // 需要代理访问的api地址 changeOrigin: true, // 允许跨域请求 }, "/acfile": { target: "http://127.0.0.1:81/", // 需要代理访问的api地址 changeOrigin: true, // 允许跨域请求 }, }, } }